diff --git a/core/pom.xml b/core/pom.xml index 2848f295e..3c5dce4f1 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ io.jenkins.pluginhealth.scoring plugin-health-scoring-parent - 2.8.1-SNAPSHOT + 3.0.0-SNAPSHOT ../ @@ -52,6 +52,10 @@ org.eclipse.jgit 6.7.0.202309050840-r + + org.apache.maven + maven-model + com.fasterxml.jackson.dataformat jackson-dataformat-yaml @@ -102,9 +106,5 @@ postgresql test - - org.apache.maven - maven-model - diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Plugin.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Plugin.java index 68ad0a862..1df2a941c 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Plugin.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Plugin.java @@ -112,7 +112,7 @@ public Map getDetails() { public Plugin addDetails(ProbeResult newProbeResult) { this.details.compute(newProbeResult.id(), (s, previousProbeResult) -> - newProbeResult.status() == ResultStatus.ERROR ? + newProbeResult.status() == ProbeResult.Status.ERROR ? null : Objects.equals(previousProbeResult, newProbeResult) ? previousProbeResult : newProbeResult ); diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ProbeResult.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ProbeResult.java index 34a105040..80218541c 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ProbeResult.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ProbeResult.java @@ -27,16 +27,20 @@ import java.time.ZonedDateTime; import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonAlias; + /** * Represents the result of one analyze performed by a {@link io.jenkins.pluginhealth.scoring.probes.Probe} implementation on a {@link Plugin} * - * @param id represent the ID of the {@link io.jenkins.pluginhealth.scoring.probes.Probe} - * @param message represents a summary of the result - * @param status represents the state of the analyze performed + * @param id represent the ID of the {@link io.jenkins.pluginhealth.scoring.probes.Probe} + * @param message represents a summary of the result + * @param status represents the state of the performed analysis + * @param timestamp when the probe generated this result + * @param probeVersion the version of the probe which generated this result */ -public record ProbeResult(String id, String message, ResultStatus status, ZonedDateTime timestamp) { - public ProbeResult(String id, String message, ResultStatus status) { - this(id, message, status, ZonedDateTime.now()); +public record ProbeResult(String id, String message, Status status, ZonedDateTime timestamp, long probeVersion) { + public ProbeResult(String id, String message, Status status, long probeVersion) { + this(id, message, status, ZonedDateTime.now(), probeVersion); } @Override @@ -44,7 +48,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProbeResult that = (ProbeResult) o; - return id.equals(that.id) && message.equals(that.message) && status == that.status; + return id.equals(that.id) && message.equals(that.message) && status == that.status && probeVersion == that.probeVersion; } @Override @@ -52,16 +56,20 @@ public int hashCode() { return Objects.hash(id, status); } - public static ProbeResult success(String id, String message) { - return new ProbeResult(id, message, ResultStatus.SUCCESS); + public static ProbeResult success(String id, String message, long probeVersion) { + return new ProbeResult(id, message, Status.SUCCESS, probeVersion); } - public static ProbeResult failure(String id, String message) { - return new ProbeResult(id, message, ResultStatus.FAILURE); + public static ProbeResult error(String id, String message, long probeVersion) { + return new ProbeResult(id, message, Status.ERROR, probeVersion); } - public static ProbeResult error(String id, String message) { - return new ProbeResult(id, message, ResultStatus.ERROR); + /* + * To handle the probe status migration from 2.x.y to 3.x.y, let consider the FAILURE status as an ERROR to force + * the execution of the probe. + */ + public enum Status { + SUCCESS, @JsonAlias("FAILURE") ERROR } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Score.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Score.java index 2b5f8a3cd..792a4359e 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Score.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/Score.java @@ -84,12 +84,12 @@ public long getValue() { private void computeValue() { var sum = details.stream() - .flatMapToDouble(res -> DoubleStream.of(res.value() * res.coefficient())) + .flatMapToDouble(res -> DoubleStream.of(res.value() * res.weight())) .sum(); var coefficient = details.stream() - .flatMapToDouble(res -> DoubleStream.of(res.coefficient())) + .flatMapToDouble(res -> DoubleStream.of(res.weight())) .sum(); - this.value = Math.round(100 * (sum / coefficient)); + this.value = Math.round((sum / coefficient)); } public void addDetail(ScoreResult result) { diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoreResult.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoreResult.java index 05d383ed8..30ca6cc84 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoreResult.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoreResult.java @@ -25,10 +25,19 @@ package io.jenkins.pluginhealth.scoring.model; import java.util.Objects; +import java.util.Set; -public record ScoreResult(String key, float value, float coefficient) { +import com.fasterxml.jackson.annotation.JsonAlias; + +/** + * @param key the identifier of the scoring implementation which produced this result + * @param value the score granted to the plugin by a specific scoring implementation, out of 100 (one hundred). + * @param weight the importance of the score facing the others + * @param componentsResults a list of {@link ScoringComponentResult} which explain the score + */ +public record ScoreResult(String key, int value, @JsonAlias("coefficient") float weight, Set componentsResults) { public ScoreResult { - if (value > 1 || coefficient > 1) { + if (weight > 1) { throw new IllegalArgumentException("Value and Coefficient must be less or equal to 1."); } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ResultStatus.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoringComponentResult.java similarity index 71% rename from core/src/main/java/io/jenkins/pluginhealth/scoring/model/ResultStatus.java rename to core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoringComponentResult.java index c7db5b740..82735491c 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ResultStatus.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/ScoringComponentResult.java @@ -24,9 +24,16 @@ package io.jenkins.pluginhealth.scoring.model; +import java.util.List; + +import io.jenkins.pluginhealth.scoring.scores.ScoringComponent; + /** - * Represents the state of the analyze performed by any {@link io.jenkins.pluginhealth.scoring.probes.Probe} implementation + * Describes the evaluation from a {@link ScoringComponent} on a specific plugin. + * + * @param score the score representing the points granted to the plugin, out of 100 (one hundred). + * @param weight the weight of the score + * @param reasons the list of string explaining the score granted to the plugin */ -public enum ResultStatus { - SUCCESS, FAILURE, ERROR +public record ScoringComponentResult(int score, float weight, List reasons) { } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractDependencyBotConfigurationProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractDependencyBotConfigurationProbe.java index 7800a17e6..156faecb9 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractDependencyBotConfigurationProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractDependencyBotConfigurationProbe.java @@ -44,21 +44,24 @@ public abstract class AbstractDependencyBotConfigurationProbe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Path scmRepository = context.getScmRepository(); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); + } + final Path scmRepository = context.getScmRepository().get(); final Path githubConfig = scmRepository.resolve(".github"); if (Files.notExists(githubConfig)) { - LOGGER.error("No GitHub configuration folder at {} ", key()); - return ProbeResult.failure(key(), "No GitHub configuration folder found"); + LOGGER.trace("No GitHub configuration folder at {} ", key()); + return this.success("No GitHub configuration folder found."); } try (Stream paths = Files.find(githubConfig, 1, (path, $) -> Files.isRegularFile(path) && path.getFileName().toString().startsWith(getBotName()))) { return paths.findFirst() - .map(file -> ProbeResult.success(key(), String.format("%s is configured", getBotName()))) - .orElseGet(() -> ProbeResult.failure(key(), String.format("%s is not configured", getBotName()))); + .map(file -> this.success(String.format("%s is configured.", getBotName()))) + .orElseGet(() -> this.success(String.format("%s is not configured.", getBotName()))); } catch (IOException ex) { LOGGER.error("Could not browse the plugin folder during probe {}", key(), ex); - return ProbeResult.error(key(), "Could not browse the plugin folder"); + return this.error("Could not browse the plugin folder"); } } @@ -74,9 +77,4 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { protected boolean isSourceCodeRelated() { return true; } - - @Override - public String[] getProbeResultRequirement() { - return new String[] { SCMLinkValidationProbe.KEY, LastCommitDateProbe.KEY }; - } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractGitHubWorkflowProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractGitHubWorkflowProbe.java index e202ad11a..eb76b03d4 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractGitHubWorkflowProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractGitHubWorkflowProbe.java @@ -50,11 +50,14 @@ public abstract class AbstractGitHubWorkflowProbe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Path repository = context.getScmRepository(); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); + } + final Path repository = context.getScmRepository().get(); final Path workflowPath = repository.resolve(WORKFLOWS_DIRECTORY); if (!Files.exists(workflowPath)) { - return ProbeResult.failure(key(), "Plugin has no GitHub Action configured"); + return this.success("Plugin has no GitHub Action configured."); } try (Stream files = Files.find(workflowPath, 1, (path, $) -> Files.isRegularFile(path))) { @@ -67,11 +70,11 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { .anyMatch(jobDefinition -> jobDefinition.startsWith(getWorkflowDefinition())); return isWorkflowConfigured ? - ProbeResult.success(key(), getSuccessMessage()) : - ProbeResult.failure(key(), getFailureMessage()); + this.success(getValidMessage()) : + this.success(getInvalidMessage()); } catch (IOException e) { LOGGER.warn("Couldn't not read file for plugin {} during probe {}", plugin.getName(), key(), e); - return ProbeResult.error(key(), e.getMessage()); + return this.error(e.getMessage()); } } @@ -111,14 +114,6 @@ private record WorkflowDefinition(Map jobs) { private record WorkflowJobDefinition(String uses) { } - /** - * @return a String array of probes that should be executed before AbstractGitHubWorkflowProbe - */ - @Override - public String[] getProbeResultRequirement() { - return new String[] { SCMLinkValidationProbe.KEY, LastCommitDateProbe.KEY }; - } - @Override protected boolean isSourceCodeRelated() { return true; @@ -132,12 +127,14 @@ protected boolean isSourceCodeRelated() { public abstract String getWorkflowDefinition(); /** - * @return a failure message + * @return a message used when the probe could browse the workflows configured on the repository, + * but not the one expected by {@link this#getWorkflowDefinition()}. */ - public abstract String getFailureMessage(); + public abstract String getInvalidMessage(); /** - * @return a success message + * @return a message used when the probe could find the expected workflow from {@link this#getWorkflowDefinition()} + * configured on the plugin repository. */ - public abstract String getSuccessMessage(); + public abstract String getValidMessage(); } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbe.java index 69b9db4de..63823d5a4 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbe.java @@ -41,12 +41,13 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +/** + * This probe is looking for the Code Coverage records, using GitHub API for Checks, on the default branch of the plugin. + */ @Component @Order(CodeCoverageProbe.ORDER) public class CodeCoverageProbe extends Probe { private static final Logger LOGGER = LoggerFactory.getLogger(CodeCoverageProbe.class); - private static final int LINE_COVERAGE_THRESHOLD = 70; - private static final int BRANCH_COVERAGE_THRESHOLD = 60; private static final String COVERAGE_TITLE_REGEXP = "^Line(?: Coverage)?: (?\\d{1,2}(?:\\.\\d{1,2})?)%(?: \\(.+\\))?. Branch(?: Coverage)?: (?\\d{1,2}(?:\\.\\d{1,2})?)%(?: \\(.+\\))?\\.?$"; @@ -60,14 +61,17 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin ucPlugin = context.getUpdateCenter().plugins().get(plugin.getName()); final String defaultBranch = ucPlugin.defaultBranch(); + if (defaultBranch == null || defaultBranch.isBlank()) { + return this.error("No default branch configured for the plugin."); + } try { - final Optional repositoryName = context.getRepositoryName(plugin.getScm()); + final Optional repositoryName = context.getRepositoryName(); if (repositoryName.isPresent()) { final GHRepository ghRepository = context.getGitHub().getRepository(repositoryName.get()); final List ghCheckRuns = ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage")).toList(); - if (ghCheckRuns.size() == 0) { - return ProbeResult.error(key(), "Could not determine code coverage for plugin"); + if (ghCheckRuns.isEmpty()) { + return this.success("Could not determine code coverage for the plugin."); } double overall_line_coverage = 100; @@ -81,18 +85,14 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { overall_branch_coverage = Math.min(overall_branch_coverage, branch_coverage); } } - return overall_line_coverage >= LINE_COVERAGE_THRESHOLD && overall_branch_coverage >= BRANCH_COVERAGE_THRESHOLD ? - ProbeResult.success(key(), "Line coverage is above " + LINE_COVERAGE_THRESHOLD + "%. Branch coverage is above " + BRANCH_COVERAGE_THRESHOLD + "%.") : - ProbeResult.failure(key(), - "Line coverage is " + (overall_line_coverage < LINE_COVERAGE_THRESHOLD ? "below " : "above ") + LINE_COVERAGE_THRESHOLD + "%. " + - "Branch coverage is " + (overall_branch_coverage < BRANCH_COVERAGE_THRESHOLD ? "below " : "above ") + BRANCH_COVERAGE_THRESHOLD + "%." - ); + + return this.success("Line coverage: " + overall_line_coverage + "%. Branch coverage: " + overall_branch_coverage + "%."); } else { - return ProbeResult.error(key(), "Cannot determine plugin repository"); + return this.error("Cannot determine plugin repository."); } } catch (IOException e) { LOGGER.warn("Could not get Coverage check for {}", plugin.getName(), e); - return ProbeResult.error(key(), "Could not get coverage check"); + return this.error("Could not get coverage check"); } } @@ -112,12 +112,7 @@ protected boolean isSourceCodeRelated() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{ - SCMLinkValidationProbe.KEY, - JenkinsfileProbe.KEY, - UpdateCenterPluginPublicationProbe.KEY, - LastCommitDateProbe.KEY, - }; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbe.java index 155f3635c..80a177bb2 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbe.java @@ -53,13 +53,17 @@ public String getWorkflowDefinition() { } @Override - public String getFailureMessage() { - return "Could not find JEP-229 workflow definition"; + public String getInvalidMessage() { + return "Could not find JEP-229 workflow definition."; } @Override - public String getSuccessMessage() { - return "JEP-229 workflow definition found"; + public String getValidMessage() { + return "JEP-229 workflow definition found."; } + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbe.java index dfd6d705d..189f399e5 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbe.java @@ -43,16 +43,20 @@ public class ContributingGuidelinesProbe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Path repository = context.getScmRepository(); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); + } + + final Path repository = context.getScmRepository().get(); try (Stream paths = Files.find(repository, 2, (file, basicFileAttributes) -> Files.isReadable(file) && ("CONTRIBUTING.md".equalsIgnoreCase(file.getFileName().toString()) || "CONTRIBUTING.adoc".equalsIgnoreCase(file.getFileName().toString())))) { return paths.findAny() - .map(file -> ProbeResult.success(key(), "Contributing guidelines found")) - .orElseGet(() -> ProbeResult.failure(key(), "No contributing guidelines found")); + .map(file -> this.success("Contributing guidelines found.")) + .orElseGet(() -> this.success("No contributing guidelines found.")); } catch (IOException e) { - return ProbeResult.error(key(), e.getMessage()); + return this.error(e.getMessage()); } } @@ -72,7 +76,7 @@ protected boolean isSourceCodeRelated() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY, LastCommitDateProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbe.java index 53a596822..7ed799a44 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbe.java @@ -49,4 +49,9 @@ public String key() { public String getDescription() { return "Checks if Dependabot is configured on a plugin."; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbe.java index 8d364e703..dadd18734 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbe.java @@ -52,22 +52,20 @@ public class DependabotPullRequestProbe extends Probe { protected ProbeResult doApply(Plugin plugin, ProbeContext context) { try { final GitHub gh = context.getGitHub(); - final GHRepository repository = gh.getRepository(context.getRepositoryName(plugin.getScm()).orElseThrow()); + final GHRepository repository = gh.getRepository(context.getRepositoryName().orElseThrow()); final List pullRequests = repository.getPullRequests(GHIssueState.OPEN); final long count = pullRequests.stream() .filter(pr -> pr.getLabels().stream().anyMatch(label -> "dependencies".equals(label.getName()))) .count(); - return count > 0 ? - ProbeResult.failure(key(), "%d open pull requests from Dependabot".formatted(count)) : - ProbeResult.success(key(), "No open pull request from dependabot"); + return this.success("%d".formatted(count)); } catch (NoSuchElementException | IOException e) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(e.getMessage()); } - return ProbeResult.error(key(), "Could not count dependabot pull requests"); + return this.error("Could not count dependabot pull requests."); } } @@ -82,7 +80,7 @@ public String getDescription() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{DependabotProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbe.java index 8264b95b7..ef37f9323 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbe.java @@ -44,16 +44,15 @@ public class DeprecatedPluginProbe extends Probe { public ProbeResult doApply(Plugin plugin, ProbeContext ctx) { final UpdateCenter updateCenter = ctx.getUpdateCenter(); if (updateCenter.deprecations().containsKey(plugin.getName())) { - return ProbeResult.failure(key(), updateCenter.deprecations().get(plugin.getName()).url()); + return this.success(updateCenter.deprecations().get(plugin.getName()).url()); } final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin updateCenterPlugin = updateCenter.plugins().get(plugin.getName()); if (updateCenterPlugin == null) { - return ProbeResult.failure(key(), "This plugin is not in update-center"); + return this.error("This plugin is not in update-center."); } - if (updateCenterPlugin.labels().contains("deprecated")) { - return ProbeResult.failure(key(), "This plugin is marked as deprecated"); - } - return ProbeResult.success(key(), "This plugin is NOT deprecated"); + return updateCenterPlugin.labels().contains("deprecated") ? + this.success("This plugin is marked as deprecated.") : + this.success("This plugin is NOT deprecated."); } @Override @@ -65,4 +64,9 @@ public String key() { public String getDescription() { return "This probe detects if a specified plugin is deprecated from the update-center."; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbe.java index 57a44f112..350619f7c 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbe.java @@ -35,22 +35,26 @@ @Component @Order(DocumentationMigrationProbe.ORDER) public class DocumentationMigrationProbe extends Probe { - public static final int ORDER = SCMLinkValidationProbe.ORDER + 100; + public static final int ORDER = DeprecatedPluginProbe.ORDER + 100; public static final String KEY = "documentation-migration"; @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { final Map pluginDocumentationLinks = context.getPluginDocumentationLinks(); + if (pluginDocumentationLinks.isEmpty()) { + return this.error("No link to documentation can be confirmed."); + } final String scm = plugin.getScm(); + if (scm == null) { + return this.error("Plugin SCM on the update-center is not correctly configured for the plugin."); + } final String linkDocumentationForPlugin = pluginDocumentationLinks.get(plugin.getName()); - return pluginDocumentationLinks.isEmpty() ? - ProbeResult.error(key(), "No link to documentation can be confirmed") : - linkDocumentationForPlugin == null ? - ProbeResult.error(key(), "Plugin is not listed in documentation migration source") : - scm != null && linkDocumentationForPlugin.contains(scm) ? - ProbeResult.success(key(), "Documentation is located in the plugin repository") : - ProbeResult.failure(key(), "Documentation is not located in the plugin repository"); + return linkDocumentationForPlugin == null ? + this.error("Plugin is not listed in documentation migration source.") : + linkDocumentationForPlugin.contains(scm) ? + this.success("Documentation is located in the plugin repository.") : + this.success("Documentation is not located in the plugin repository."); } @Override @@ -69,7 +73,7 @@ protected boolean requiresRelease() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbe.java index fd7410904..0b6c53d8d 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbe.java @@ -24,9 +24,8 @@ package io.jenkins.pluginhealth.scoring.probes; -import static io.jenkins.pluginhealth.scoring.probes.SCMLinkValidationProbe.GH_PATTERN; - import java.io.IOException; +import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; @@ -34,7 +33,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; import java.util.stream.Collectors; import io.jenkins.pluginhealth.scoring.model.Plugin; @@ -63,12 +61,14 @@ public class HasUnreleasedProductionChangesProbe extends Probe { @Override public ProbeResult doApply(Plugin plugin, ProbeContext context) { - Matcher matcher = GH_PATTERN.matcher(plugin.getScm()); - if (!matcher.find()) { - return ProbeResult.error(key(), "SCM link doesn't match GitHub plugin repositories"); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); } - final Optional folder = context.getScmFolderPath(); + + final Path repo = context.getScmRepository().get(); + final Optional folder = context.getScmFolderPath(); final Set files = new HashSet<>(); + final List paths = new ArrayList<>(3); paths.add("pom.xml"); @@ -79,7 +79,7 @@ public ProbeResult doApply(Plugin plugin, ProbeContext context) { paths.add("src/main"); } - try (Git git = Git.open(context.getScmRepository().toFile())) { + try (Git git = Git.open(repo.toFile())) { LogCommand logCommand = git.log(); paths.forEach(logCommand::addPath); for (RevCommit revCommit : logCommand.call()) { @@ -115,19 +115,14 @@ public ProbeResult doApply(Plugin plugin, ProbeContext context) { } return files.isEmpty() ? - ProbeResult.success(KEY, "All production modifications were released.") : - ProbeResult.failure(KEY, "Unreleased production modifications might exist in the plugin source code at " + this.success("All production modifications were released.") : + this.success("Unreleased production modifications might exist in the plugin source code at " + files.stream().sorted(Comparator.naturalOrder()).collect(Collectors.joining(", "))); } catch (IOException | GitAPIException ex) { - return ProbeResult.error(KEY, ex.getMessage()); + return this.error(ex.getMessage()); } } - @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY, LastCommitDateProbe.KEY}; - } - @Override public String key() { return KEY; @@ -147,4 +142,9 @@ protected boolean isSourceCodeRelated() { */ return false; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbe.java index 813627dd3..be0e71145 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbe.java @@ -62,11 +62,13 @@ public class IncrementalBuildDetectionProbe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Path scmRepository = context.getScmRepository(); - final Path mvnConfig = scmRepository.resolve(".mvn"); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); + } + final Path mvnConfig = context.getScmRepository().get().resolve(".mvn"); if (Files.notExists(mvnConfig)) { LOGGER.info("Could not find Maven configuration folder {} plugin while running {} probe.", plugin.getName(), key()); - return ProbeResult.failure(key(), String.format("Could not find Maven configuration folder for the %s plugin.", plugin.getName())); + return this.error(String.format("Could not find Maven configuration folder for the %s plugin.", plugin.getName())); } try (Stream incrementalConfigsStream = Files.find(mvnConfig, 1, (path, $) -> Files.isRegularFile(path) && (path.endsWith("maven.config") || path.endsWith("extensions.xml")))) { @@ -80,18 +82,14 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { if (mavenExtensionsFile.isPresent() && mavenConfigFile.isPresent()) { return isExtensionsXMLConfigured(mavenExtensionsFile.get()) && isMavenConfigConfigured(mavenConfigFile.get()) - ? ProbeResult.success(key(), String.format("Incremental Build is configured in the %s plugin.", plugin.getName())) - : ProbeResult.failure(key(), String.format("Incremental Build is not configured in the %s plugin.", plugin.getName())); + ? this.success(String.format("Incremental Build is configured in the %s plugin.", plugin.getName())) + : this.success(String.format("Incremental Build is not configured in the %s plugin.", plugin.getName())); } } catch (IOException e) { LOGGER.error("Could not read files from .mvn directory for {} plugin while running {} probe.", plugin.getName(), key()); + return this.error("Could not access files in .mvn directory."); } - return ProbeResult.failure(key(), String.format("Incremental Build is not configured in the %s plugin.", plugin.getName())); - } - - @Override - public String[] getProbeResultRequirement() { - return new String[]{ContinuousDeliveryProbe.KEY}; + return this.success(String.format("Incremental Build is not configured in the %s plugin.", plugin.getName())); } @Override @@ -124,9 +122,9 @@ private boolean isExtensionsXMLConfigured(Path path) { String artifactId = artifactIdElement.getTextContent(); return INCREMENTAL_TOOL.equals(groupId) && INCREMENTAL_TOOL_ARTIFACT_ID.equals(artifactId); } catch (IOException e) { - LOGGER.error("Could not read the file during probe {}. {}", key(), e); + LOGGER.error("Could not read the file during probe {}.", key(), e); } catch (ParserConfigurationException | SAXException e) { - LOGGER.error("Could not parse the file during probe {}. {}", key(), e); + LOGGER.error("Could not parse the file during probe {}.", key(), e); } return false; } @@ -141,8 +139,13 @@ private boolean isMavenConfigConfigured(Path path) { try { return Files.readAllLines(path).containsAll(List.of("-Pconsume-incrementals", "-Pmight-produce-incrementals")); } catch (IOException e) { - LOGGER.error("Could not read the file during probe {}. {}", key(), e); + LOGGER.error("Could not read the file during probe {}.", key(), e); } return false; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbe.java index 8b3ae1fa2..6edb88cb7 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbe.java @@ -41,7 +41,9 @@ public class InstallationStatProbe extends Probe { protected ProbeResult doApply(Plugin plugin, ProbeContext context) { final UpdateCenter updateCenter = context.getUpdateCenter(); final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin ucPlugin = updateCenter.plugins().get(plugin.getName()); - return ProbeResult.success(KEY, "%d".formatted(ucPlugin.popularity())); + return ucPlugin != null ? + this.success("%d".formatted(ucPlugin.popularity())) : + this.error("Could not find plugin " + plugin.getName() + " in Update Center."); } @Override @@ -55,7 +57,7 @@ public String getDescription() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{UpdateCenterPluginPublicationProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JSR305Probe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JSR305Probe.java index 8cfb4a87f..e015b18f8 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JSR305Probe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JSR305Probe.java @@ -29,7 +29,10 @@ public class JSR305Probe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Path scmRepository = context.getScmRepository(); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); + } + final Path scmRepository = context.getScmRepository().get(); /* The "maxDepth" is set to Integer.MAX_VALUE because a repository can have multiple modules with class files in it. We do not want to miss any ".java" file. */ try (Stream javaFiles = Files.find(scmRepository, Integer.MAX_VALUE, (path, $) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".java"))) { @@ -39,19 +42,19 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { .collect(Collectors.toSet()); return javaFilesWithDetectedImports.isEmpty() - ? ProbeResult.success(key(), String.format(getSuccessMessage() + " at %s plugin.", plugin.getName())) - : ProbeResult.failure(key(), String.format(getFailureMessage() + " at %s plugin for ", plugin.getName()) + javaFilesWithDetectedImports.stream().sorted(Comparator.naturalOrder()).collect(Collectors.joining(", "))); + ? this.success(String.format("%s at %s plugin.", getValidMessage(), plugin.getName())) + : this.success(String.format( + "%s at %s plugin for %s", + getInvalidMessage(), + plugin.getName(), + javaFilesWithDetectedImports.stream().sorted(Comparator.naturalOrder()).collect(Collectors.joining(", ")) + )); } catch (IOException ex) { - LOGGER.error("Could not browse the plugin folder during {} probe. {}", key(), ex); - return ProbeResult.error(key(), String.format("Could not browse the plugin folder during {} probe.", plugin.getName())); + LOGGER.error("Could not browse the plugin folder during {} probe.", key(), ex); + return this.error(String.format("Could not browse the plugin folder during %s probe.", plugin.getName())); } } - @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY}; - } - @Override public String key() { return KEY; @@ -62,7 +65,6 @@ public String getDescription() { return "The probe checks for deprecated annotations."; } - /** * Fetches all the import for the given file. * @@ -73,7 +75,7 @@ private List getAllImportsInTheFile(Path javaFile) { try (Stream importStatements = Files.lines(javaFile).filter(line -> line.startsWith("import")).map(this::getFullyQualifiedImportName)) { return importStatements.toList(); } catch (IOException ex) { - LOGGER.error("Could not browse the {} plugin folder during probe. {}", key(), ex); + LOGGER.error("Could not browse the {} plugin folder during probe.", key(), ex); } return List.of(); } @@ -88,14 +90,14 @@ public String getImportToCheck() { /** * @return a success message. */ - String getSuccessMessage() { + private String getValidMessage() { return "Latest version of imports found"; } /** * @return a failure message. */ - String getFailureMessage() { + private String getInvalidMessage() { return "Deprecated imports found"; } @@ -119,4 +121,9 @@ private boolean containsImports(Path javaFile) { List imports = getAllImportsInTheFile(javaFile); return imports.stream().anyMatch(line -> line.startsWith(getImportToCheck())); } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbe.java index f28e7e587..41cf252e4 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbe.java @@ -41,7 +41,9 @@ public class JenkinsCoreProbe extends Probe { protected ProbeResult doApply(Plugin plugin, ProbeContext context) { final UpdateCenter uc = context.getUpdateCenter(); final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin ucPlugin = uc.plugins().get(plugin.getName()); - return ProbeResult.success(KEY, ucPlugin.requiredCore()); + return ucPlugin != null ? + this.success(ucPlugin.requiredCore()) : + this.error("Could not find plugin " + plugin.getName() + " in Update Center."); } @Override @@ -60,7 +62,7 @@ protected boolean requiresRelease() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{UpdateCenterPluginPublicationProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbe.java index 2f3c76680..bafeaca78 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbe.java @@ -43,15 +43,18 @@ public class JenkinsfileProbe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Path repository = context.getScmRepository(); - try (Stream paths = Files - .find(repository, 1, (file, basicFileAttributes) -> Files.isReadable(file) && "Jenkinsfile".equals(file.getFileName().toString())) - ) { + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); + } + + final Path repository = context.getScmRepository().get(); + try (Stream paths = Files.find(repository, 1, (file, $) -> + Files.isReadable(file) && "Jenkinsfile".equals(file.getFileName().toString()))) { return paths.findFirst() - .map(file -> ProbeResult.success(key(), "Jenkinsfile found")) - .orElseGet(() -> ProbeResult.failure(key(), "No Jenkinsfile found")); + .map(file -> this.success("Jenkinsfile found")) + .orElseGet(() -> this.success("No Jenkinsfile found")); } catch (IOException e) { - return ProbeResult.error(key(), e.getMessage()); + return this.error(e.getMessage()); } } @@ -74,7 +77,7 @@ protected boolean isSourceCodeRelated() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY, LastCommitDateProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbe.java index 28918cbe1..a2e69eaf4 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbe.java @@ -59,8 +59,8 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { .collect(Collectors.joining(", ")); return !issues.isBlank() ? - ProbeResult.failure(key(), issues) : - ProbeResult.success(key(), "Plugin is OK"); + this.success(issues) : + this.success("No known security vulnerabilities."); } @Override @@ -72,4 +72,9 @@ public String key() { public String getDescription() { return "Detects if a given plugin has a public security vulnerability advertised in a security advisory."; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbe.java index 0ed4117c4..a164786f4 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbe.java @@ -24,9 +24,13 @@ package io.jenkins.pluginhealth.scoring.probes; +import java.io.IOException; +import java.nio.file.Path; +import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.Optional; -import java.util.regex.Matcher; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; @@ -52,39 +56,34 @@ public class LastCommitDateProbe extends Probe { @Override public ProbeResult doApply(Plugin plugin, ProbeContext context) { - final Matcher matcher = SCMLinkValidationProbe.GH_PATTERN.matcher(plugin.getScm()); - if (!matcher.find()) { - return ProbeResult.failure(key(), "The SCM link is not valid"); + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + plugin.getName() + "."); } - final String repo = String.format("https://%s/%s", matcher.group("server"), matcher.group("repo")); - final Optional folder = context.getScmFolderPath(); - - try (Git git = Git.cloneRepository().setURI(repo).setDirectory(context.getScmRepository().toFile()).call()) { + final Path scmRepository = context.getScmRepository().get(); + final Optional folder = context.getScmFolderPath(); + try (Git git = Git.open(scmRepository.toFile())) { final LogCommand logCommand = git.log().setMaxCount(1); - if (folder.isPresent()) { + if (folder.isPresent() && !folder.get().toString().isBlank()) { logCommand.addPath(folder.get().toString()); } + final RevCommit commit = logCommand.call().iterator().next(); if (commit == null) { - return ProbeResult.failure(key(), "Last commit cannot be extracted. Please validate sub-folder if any."); + return this.error("Last commit cannot be extracted. Please validate sub-folder if any."); } final ZonedDateTime commitDate = ZonedDateTime.ofInstant( - commit.getAuthorIdent().getWhenAsInstant(), - commit.getAuthorIdent().getZoneId() - ); + commit.getAuthorIdent().getWhenAsInstant(), + commit.getAuthorIdent().getZoneId() + ).withZoneSameInstant(ZoneId.of("UTC")) + .truncatedTo(ChronoUnit.SECONDS); context.setLastCommitDate(commitDate); - return ProbeResult.success(key(), commitDate.toString()); - } catch (GitAPIException ex) { - LOGGER.error("There was an issue while cloning the plugin repository", ex); - return ProbeResult.failure(key(), "Could not clone the plugin repository"); + return this.success(commitDate.format(DateTimeFormatter.ISO_DATE_TIME)); + } catch (IOException | GitAPIException ex) { + LOGGER.error("There was an issue while accessing the plugin repository", ex); + return this.error("Could not access the plugin repository."); } } - @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY}; - } - @Override public String key() { return KEY; @@ -104,4 +103,9 @@ protected boolean isSourceCodeRelated() { */ return false; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/Probe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/Probe.java index 305be590b..4e144ab3b 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/Probe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/Probe.java @@ -24,12 +24,12 @@ package io.jenkins.pluginhealth.scoring.probes; +import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,22 +55,23 @@ public final ProbeResult apply(Plugin plugin, ProbeContext context) { } return doApply(plugin, context); } - return ProbeResult.error(key(), key() + " does not meet the criteria to be executed on " + plugin.getName()); + final ProbeResult lastResult = plugin.getDetails().get(key()); + return lastResult != null ? + lastResult : + this.error(key() + " was not executed on " + plugin.getName()); } private boolean shouldBeExecuted(Plugin plugin, ProbeContext context) { - for (String requirementKey : this.getProbeResultRequirement()) { - final ProbeResult probeResult = plugin.getDetails().get(requirementKey); - if (probeResult == null || probeResult.status().equals(ResultStatus.FAILURE)) { - LOGGER.info("{} requires {} on {} before being executed", this.key(), requirementKey, plugin.getName()); - return false; - } - } - final ProbeResult previousResult = plugin.getDetails().get(this.key()); if (previousResult == null) { return true; } + if (this.getVersion() != previousResult.probeVersion()) { + return true; + } + if (ProbeResult.Status.ERROR.equals(previousResult.status())) { + return true; + } if (!this.requiresRelease() && !this.isSourceCodeRelated()) { return true; } @@ -78,17 +79,22 @@ private boolean shouldBeExecuted(Plugin plugin, ProbeContext context) { (previousResult.timestamp() != null && previousResult.timestamp().isBefore(plugin.getReleaseTimestamp()))) { return true; } - final Optional optionalLastCommit = context.getLastCommitDate(); - if (this.isSourceCodeRelated() && optionalLastCommit.isEmpty()) { - LOGGER.error( - "{} requires last commit date for {} but was executed before the date time is registered in execution context", + final Optional optionalScmRepository = context.getScmRepository(); + if (this.isSourceCodeRelated() && optionalScmRepository.isEmpty()) { + LOGGER.info( + "{} requires the SCM for {} but the SCM was not cloned locally", this.key(), plugin.getName() ); + return false; } + final Optional optionalLastCommit = context.getLastCommitDate(); if (this.isSourceCodeRelated() && optionalLastCommit .map(date -> previousResult.timestamp() != null && previousResult.timestamp().isBefore(date)) - .orElse(false)) { + .orElseGet(() -> { + LOGGER.info("{} is based on code modification but last commit for {} is unknown. It will be executed.", key(), plugin.getName()); + return true; + })) { return true; } @@ -106,20 +112,9 @@ private boolean shouldBeExecuted(Plugin plugin, ProbeContext context) { */ protected abstract ProbeResult doApply(Plugin plugin, ProbeContext context); - /** - * List of probe key to be present in the {@link Plugin#details} map and to be {@link ResultStatus#SUCCESS} in - * order to consider executing the {@link Probe#doApply(Plugin, ProbeContext)} code. - * By default, the requirement is an empty array. It cannot be null. - * - * @return array of {@link Probe#key()} to be present in {@link Plugin#details}. - */ - public String[] getProbeResultRequirement() { - return new String[]{}; - } - /** * Returns the key identifier for the probe. - * This is how the different probes can be identified in the {@link Plugin#details} map. + * This is how the different probes can be identified in the {@link Plugin#getDetails()} map. * * @return the identifier of the probe */ @@ -152,4 +147,28 @@ protected boolean requiresRelease() { protected boolean isSourceCodeRelated() { return false; } + + public abstract long getVersion(); + + /** + * Helper method to create a {@link ProbeResult} with a {@link ProbeResult.Status#SUCCESS} status, using the provided + * message and the {@link this#key()} and {@link this#getVersion()} values. + * + * @param message the message to be stored in the returned {@link ProbeResult} + * @return a {@link ProbeResult} with a success status, the provided message and the probe version + */ + public final ProbeResult success(String message) { + return ProbeResult.success(this.key(), message, this.getVersion()); + } + + /** + * Helper method to create a {@link ProbeResult} with a {@link ProbeResult.Status#ERROR} status, using the provided + * message and the {@link this#key()} and {@link this#getVersion()} values. + * + * @param message the message to be stored in the returned {@link ProbeResult} + * @return a {@link ProbeResult} with a error status, the provided message and the probe version + */ + public final ProbeResult error(String message) { + return ProbeResult.error(this.key(), message, this.getVersion()); + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java index ba5e8694a..a3aa7a575 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.time.ZonedDateTime; import java.util.Comparator; import java.util.Map; @@ -35,29 +36,58 @@ import java.util.regex.Matcher; import java.util.stream.Stream; +import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; import org.kohsuke.github.GitHub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ProbeContext { + private static final Logger LOGGER = LoggerFactory.getLogger(ProbeContext.class); + + private final Plugin plugin; private final UpdateCenter updateCenter; - private final Path scmRepository; + private Path scmRepository; private GitHub github; private ZonedDateTime lastCommitDate; private Map pluginDocumentationLinks; - private Optional scmFolderPath; + private Path scmFolderPath; - public ProbeContext(String pluginName, UpdateCenter updateCenter) throws IOException { + public ProbeContext(Plugin plugin, UpdateCenter updateCenter) throws IOException { + this.plugin = plugin; this.updateCenter = updateCenter; - this.scmRepository = Files.createTempDirectory(pluginName); } public UpdateCenter getUpdateCenter() { return updateCenter; } - public Path getScmRepository() { - return scmRepository; + public void cloneRepository() { + if (scmRepository != null) { + LOGGER.warn("The Git repository of this plugin was already cloned in {}.", scmRepository); + } + if (plugin.getScm() == null || plugin.getScm().isBlank()) { + LOGGER.info("Cannot clone repository for {} because SCM link is `{}`", plugin.getName(), plugin.getScm()); + return; + } + final String pluginName = this.plugin.getName(); + try { + final Path repo = Files.createTempDirectory(pluginName); + try (Git git = Git.cloneRepository().setURI(plugin.getScm()).setDirectory(repo.toFile()).call()) { + this.scmRepository = Paths.get(git.getRepository().getDirectory().getParentFile().toURI()); + } catch (GitAPIException e) { + LOGGER.warn("Could not clone Git repository for plugin {}", pluginName, e); + } + } catch (IOException e) { + LOGGER.warn("Could not create temporary folder for plugin {}", pluginName, e); + } + } + + public Optional getScmRepository() { + return Optional.ofNullable(scmRepository); } public Optional getLastCommitDate() { @@ -84,24 +114,29 @@ public Map getPluginDocumentationLinks() { return pluginDocumentationLinks; } - public Optional getRepositoryName(String scm) { - final Matcher match = SCMLinkValidationProbe.GH_PATTERN.matcher(scm); + public Optional getRepositoryName() { + if (plugin.getScm() == null || plugin.getScm().isBlank()) { + return Optional.empty(); + } + final Matcher match = SCMLinkValidationProbe.GH_PATTERN.matcher(plugin.getScm()); return match.find() ? Optional.of(match.group("repo")) : Optional.empty(); } - public Optional getScmFolderPath() { - return scmFolderPath; + public Optional getScmFolderPath() { + return Optional.ofNullable(scmFolderPath); } - public void setScmFolderPath(Optional scmFolderPath) { + public void setScmFolderPath(Path scmFolderPath) { this.scmFolderPath = scmFolderPath; } /* default */ void cleanUp() throws IOException { - try (Stream paths = Files.walk(this.scmRepository)) { - paths.sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); + if (scmRepository != null) { + try (Stream paths = Files.walk(this.scmRepository)) { + paths.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } } } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbe.java index cbfc5cd2a..f7f8f7ea6 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbe.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; @@ -45,13 +46,20 @@ public class PullRequestProbe extends Probe { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext context) { + if (plugin.getScm() == null) { + return this.error("Plugin SCM is unknown, cannot fetch the number of open pull requests."); + } try { final GitHub gh = context.getGitHub(); - final GHRepository repository = gh.getRepository(context.getRepositoryName(plugin.getScm()).orElseThrow()); + final Optional repositoryName = context.getRepositoryName(); + if (repositoryName.isEmpty()) { + return this.error("Cannot find repository for " + plugin.getName()); + } + final GHRepository repository = gh.getRepository(repositoryName.get()); final List pullRequests = repository.getPullRequests(GHIssueState.OPEN); - return ProbeResult.success(key(), "%d".formatted(pullRequests.size())); + return this.success("%d".formatted(pullRequests.size())); } catch (IOException e) { - return ProbeResult.error(key(), e.getMessage()); + return this.error("Cannot access repository " + plugin.getScm()); } } @@ -66,7 +74,7 @@ public String getDescription() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{SCMLinkValidationProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbe.java index e8e01dce0..ae2033f8b 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbe.java @@ -25,4 +25,9 @@ public String key() { public String getDescription() { return "Checks if Renovate is configured in a plugin."; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbe.java index d58377eec..8af521f9a 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbe.java @@ -63,13 +63,14 @@ public class SCMLinkValidationProbe extends Probe { public ProbeResult doApply(Plugin plugin, ProbeContext context) { if (plugin.getScm() == null || plugin.getScm().isBlank()) { LOGGER.warn("{} has no SCM link", plugin.getName()); - return ProbeResult.error(key(), "The plugin SCM link is empty."); + return this.error("The plugin SCM link is empty."); } return fromSCMLink(context, plugin.getScm(), plugin.getName()); } /** - * Validates the SCM link, and sets {@link ProbeContext#setScmFolderPath(Optional)}. The value is always the path of the POM file. + * Validates the SCM link, and sets {@link ProbeContext#setScmFolderPath(Path)}. + * The value is always the path of the POM file. * * @param context Refer {@link ProbeContext}. * @param scm The SCM link {@link Plugin#getScm()}. @@ -80,19 +81,23 @@ private ProbeResult fromSCMLink(ProbeContext context, String scm, String pluginN Matcher matcher = GH_PATTERN.matcher(scm); if (!matcher.find()) { LOGGER.atDebug().log(() -> String.format("%s is not respecting the SCM URL Template.", scm)); - return ProbeResult.failure(key(), "SCM link doesn't match GitHub plugin repositories."); + return this.error("SCM link doesn't match GitHub plugin repositories."); + } + if (context.getScmRepository().isEmpty()) { + return this.error("There is no local repository for plugin " + pluginName + "."); } try { - context.getGitHub().getRepository(matcher.group("repo")); // clones the repository, fetches the repo path using the regex Matcher - Optional pluginPathInRepository = findPluginPom(context.getScmRepository(), pluginName); + context.getGitHub().getRepository(matcher.group("repo")); + Optional pluginPathInRepository = findPluginPom(context.getScmRepository().get(), pluginName); Optional folderPath = pluginPathInRepository.map(path -> path.getParent()); if (folderPath.isEmpty()) { - return ProbeResult.error(key(), String.format("No valid POM file found in %s plugin.", pluginName)); + return this.success(String.format("No valid POM file found in %s plugin.", pluginName)); } - context.setScmFolderPath(folderPath.map(path -> path.getFileName().toString())); - return ProbeResult.success(key(), "The plugin SCM link is valid."); + final Path pluginFolderPath = context.getScmRepository().get().relativize(folderPath.get()); + context.setScmFolderPath(pluginFolderPath.getFileName()); + return this.success("The plugin SCM link is valid."); } catch (IOException ex) { - return ProbeResult.failure(key(), "The plugin SCM link is invalid."); + return this.success("The plugin SCM link is invalid."); } } @@ -118,7 +123,7 @@ private Optional findPluginPom(Path directory, String pluginName) { .filter(pom -> pomFileMatchesPlugin(pom, pluginName)) .findFirst(); } catch (IOException e) { - LOGGER.error("Could not browse the folder during probe {}. {}", pluginName, e); + LOGGER.error("Could not browse the folder during probe {}.", pluginName, e); } return Optional.empty(); } @@ -146,11 +151,6 @@ private boolean pomFileMatchesPlugin(Path pomFilePath, String pluginName) { return false; } - @Override - public String[] getProbeResultRequirement() { - return new String[]{UpdateCenterPluginPublicationProbe.KEY}; - } - @Override public String key() { return KEY; @@ -168,4 +168,9 @@ public String getDescription() { protected boolean requiresRelease() { return true; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbe.java index 6f6b47f5b..19eb9ee72 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbe.java @@ -53,12 +53,17 @@ public String getWorkflowDefinition() { } @Override - public String getFailureMessage() { - return "GitHub workflow security scan is not configured in the plugin"; + public String getInvalidMessage() { + return "GitHub workflow security scan is not configured in the plugin."; } @Override - public String getSuccessMessage() { - return "GitHub workflow security scan is configured in the plugin"; + public String getValidMessage() { + return "GitHub workflow security scan is configured in the plugin."; + } + + @Override + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbe.java index 9c73603a3..8a398cd1a 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbe.java @@ -51,23 +51,26 @@ protected ProbeResult doApply(Plugin plugin, ProbeContext context) { final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin ucPlugin = context.getUpdateCenter().plugins().get(plugin.getName()); final String defaultBranch = ucPlugin.defaultBranch(); + if (defaultBranch == null || defaultBranch.isBlank()) { + return this.error("No default branch configured for the plugin."); + } try { - final Optional repositoryName = context.getRepositoryName(plugin.getScm()); + final Optional repositoryName = context.getRepositoryName(); if (repositoryName.isPresent()) { final GHRepository ghRepository = context.getGitHub().getRepository(repositoryName.get()); final List ghCheckRuns = ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "SpotBugs")).toList(); if (ghCheckRuns.size() != 1) { - return ProbeResult.failure(key(), "SpotBugs not found in build configuration"); + return this.success("SpotBugs not found in build configuration."); } else { - return ProbeResult.success(key(), "SpotBugs found in build configuration"); + return this.success("SpotBugs found in build configuration."); } } else { - return ProbeResult.failure(key(), "Cannot determine plugin repository"); + return this.error("Cannot determine plugin repository."); } } catch (IOException e) { - LOGGER.warn("Could not get SpotBugs check for {}", plugin.getName(), e); - return ProbeResult.error(key(), "Could not get SpotBugs check"); + LOGGER.debug("Could not get SpotBugs check for {}", plugin.getName(), e); + return this.error("Could not get SpotBugs check. " + e.getMessage()); } } @@ -87,7 +90,7 @@ protected boolean isSourceCodeRelated() { } @Override - public String[] getProbeResultRequirement() { - return new String[]{JenkinsfileProbe.KEY, UpdateCenterPluginPublicationProbe.KEY, LastCommitDateProbe.KEY}; + public long getVersion() { + return 1; } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbe.java index 96be71ab6..97d488700 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbe.java @@ -49,12 +49,12 @@ public ProbeResult doApply(Plugin plugin, ProbeContext context) { final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin pl = updateCenter.plugins().get(plugin.getName()); if (pl == null) { LOGGER.info("Couldn't not find {} in update-center", plugin.getName()); - return ProbeResult.failure(key(), "This plugin is not in the update-center"); + return this.error("This plugin is not in the update-center."); } if (pl.labels().contains("adopt-this-plugin")) { - return ProbeResult.failure(key(), "This plugin is up for adoption"); + return this.success("This plugin is up for adoption."); } - return ProbeResult.success(key(), "This plugin is NOT up for adoption"); + return this.success("This plugin is not up for adoption."); } @Override @@ -66,4 +66,9 @@ public String key() { public String getDescription() { return "This probe detects if a specified plugin is declared as up for adoption."; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbe.java index d0e995255..0aa97cbf8 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbe.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbe.java @@ -46,10 +46,10 @@ public ProbeResult doApply(Plugin plugin, ProbeContext ctx) { final io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin updateCenterPlugin = updateCenter.plugins().get(plugin.getName()); if (updateCenterPlugin == null) { - return ProbeResult.failure(key(), "This plugin's publication has been stopped by the update-center"); + return this.error("This plugin's publication has been stopped by the update-center."); } - return ProbeResult.success(key(), "This plugin is still actively published by the update-center"); + return this.success("This plugin is still actively published by the update-center."); } @Override @@ -61,5 +61,10 @@ public String key() { public String getDescription() { return "This probe detects if a specified plugin is still actively published by the update-center."; } + + @Override + public long getVersion() { + return 1; + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoring.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoring.java index 216341406..6999e4327 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoring.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoring.java @@ -25,14 +25,16 @@ package io.jenkins.pluginhealth.scoring.scores; import java.time.Duration; +import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.Map; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; -import io.jenkins.pluginhealth.scoring.model.ScoreResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; import io.jenkins.pluginhealth.scoring.probes.LastCommitDateProbe; import io.jenkins.pluginhealth.scoring.probes.UpForAdoptionProbe; @@ -43,31 +45,157 @@ public class AdoptionScoring extends Scoring { private static final float COEFFICIENT = 0.8f; private static final String KEY = "adoption"; - @Override - public ScoreResult apply(Plugin plugin) { - final ScoreResult result = super.apply(plugin); - if (result.value() == 0) { - return result; + private abstract static class TimeSinceLastCommitScoringComponent implements ScoringComponent { + public final Duration getTimeBetweenLastCommitAndDate(String lastCommitDateMessage, ZonedDateTime then) { + final ZonedDateTime commitDate = ZonedDateTime + .parse(lastCommitDateMessage, DateTimeFormatter.ISO_DATE_TIME) + .withZoneSameInstant(getZone()); + return Duration.between(then, commitDate).abs(); } - final ProbeResult lastCommitProbeResult = plugin.getDetails().get(LastCommitDateProbe.KEY); - if (lastCommitProbeResult != null && lastCommitProbeResult.status().equals(ResultStatus.SUCCESS)) { - final String message = lastCommitProbeResult.message(); - final ZonedDateTime commitDateTime = ZonedDateTime.parse(message); - - final Duration between = Duration.between(plugin.getReleaseTimestamp().toInstant(), commitDateTime.toInstant()); - if (between.toDays() <= Duration.of(6 * 30, ChronoUnit.DAYS).toDays()) { // Less than 6 months - return new ScoreResult(KEY, 1, COEFFICIENT); - } else if (between.toDays() < Duration.of(365, ChronoUnit.DAYS).toDays()) { // Less than a year - return new ScoreResult(KEY, .75f, COEFFICIENT); - } else if (between.toDays() < Duration.of(2 * 365, ChronoUnit.DAYS).toDays()) { // Less than 2 years - return new ScoreResult(KEY, .5f, COEFFICIENT); - } else if (between.toDays() < Duration.of(4 * 365, ChronoUnit.DAYS).toDays()) { // Less than 4 years - return new ScoreResult(KEY, .25f, COEFFICIENT); - } + protected ZoneId getZone() { + return ZoneId.of("UTC"); + } + + @Override + public int getWeight() { + return 1; } + } + + @Override + public List getComponents() { + return List.of( + new ScoringComponent() { + @Override + public String getDescription() { + return "The plugin must not be marked as up for adoption."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(UpForAdoptionProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-1000, 1000, List.of("Cannot determine if the plugin is up for adoption.")); + } + + return switch (probeResult.message()) { + case "This plugin is not up for adoption." -> + new ScoringComponentResult(100, getWeight(), List.of("The plugin is not marked as up for adoption")); + case "This plugin is up for adoption." -> + new ScoringComponentResult(-1000, getWeight(), List.of("The plugin is marked as up for adoption")); + default -> new ScoringComponentResult(-100, getWeight(), List.of()); + }; + } + + @Override + public int getWeight() { + return 1; + } + }, + new TimeSinceLastCommitScoringComponent() { + @Override + public String getDescription() { + return "There must be less than 6 months between the last commit and the last release."; + } + + @Override + public ScoringComponentResult getScore(Plugin plugin, Map probeResults) { + final ProbeResult probeResult = probeResults.get(LastCommitDateProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine the last commit date.")); + } - return new ScoreResult(KEY, 0, COEFFICIENT); + final long days = getTimeBetweenLastCommitAndDate(probeResult.message(), plugin.getReleaseTimestamp().withZoneSameInstant(getZone())).toDays(); + if (days <= Duration.of(6 * 30, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult(100, getWeight(), List.of("There is less than 6 months between the last release and the last commit.")); + } + return new ScoringComponentResult(0, getWeight(), List.of("There is more than 6 months between the last release and the last release.")); + } + }, + new TimeSinceLastCommitScoringComponent() { + @Override + public String getDescription() { + return "There must be between 6 months and 1 year between the last commit and the last release."; + } + + @Override + public ScoringComponentResult getScore(Plugin plugin, Map probeResults) { + final ProbeResult probeResult = probeResults.get(LastCommitDateProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine the last commit date.")); + } + final long days = getTimeBetweenLastCommitAndDate(probeResult.message(), plugin.getReleaseTimestamp().withZoneSameInstant(getZone())).toDays(); + if (days <= Duration.of(365, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult(100, getWeight(), List.of("There is less than a year between the last commit and the last release.")); + } + return new ScoringComponentResult(0, getWeight(), List.of("There is no commit within 6 months to 1 year since the last release.")); + } + }, + new TimeSinceLastCommitScoringComponent() { + @Override + public String getDescription() { + return "There must be a commit between 1 year and 2 years since the last release."; + } + + @Override + public ScoringComponentResult getScore(Plugin plugin, Map probeResults) { + final ProbeResult probeResult = probeResults.get(LastCommitDateProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine the last commit date.")); + } + final long days = getTimeBetweenLastCommitAndDate(probeResult.message(), plugin.getReleaseTimestamp().withZoneSameInstant(getZone())).toDays(); + if (days <= Duration.of(2 * 365, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult(100, getWeight(), List.of("There is less than 2 years between the last commit and the last release.")); + } + return new ScoringComponentResult(0, getWeight(), List.of("No commit in the last 2 years.")); + } + }, + new TimeSinceLastCommitScoringComponent() { + @Override + public String getDescription() { + return "There must be a commit between 2 years and 4 years since the last release."; + } + + @Override + public ScoringComponentResult getScore(Plugin plugin, Map probeResults) { + final ProbeResult probeResult = probeResults.get(LastCommitDateProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine the last commit date.")); + } + final long days = getTimeBetweenLastCommitAndDate(probeResult.message(), plugin.getReleaseTimestamp().withZoneSameInstant(getZone())).toDays(); + if (days <= Duration.of(4 * 365, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult(100, getWeight(), List.of("There is less than 4 years between the last commit and the last release.")); + } + return new ScoringComponentResult(0, getWeight(), List.of("No commit in the last 4 years.")); + } + }, + new TimeSinceLastCommitScoringComponent() { + @Override + public String getDescription() { + return "It must have less than 4 years between the last commit and the last release."; + } + + @Override + public ScoringComponentResult getScore(Plugin plugin, Map probeResults) { + final ProbeResult probeResult = probeResults.get(LastCommitDateProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine the last commit date.")); + } + final long days = getTimeBetweenLastCommitAndDate(probeResult.message(), plugin.getReleaseTimestamp().withZoneSameInstant(getZone())).toDays(); + if (days > Duration.of(4 * 365, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult(-1000, 100, List.of("There is more than 4 years between the last commit and the last release.")); + } + return new ScoringComponentResult(0, getWeight(), List.of("No commit in the last 4 years.")); + + } + + @Override + public int getWeight() { + return 0; + } + } + ); } @Override @@ -76,16 +204,10 @@ public String key() { } @Override - public float coefficient() { + public float weight() { return COEFFICIENT; } - @Override - public Map getScoreComponents() { - return Map.of( - UpForAdoptionProbe.KEY, 1f - ); - } @Override public String description() { diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoring.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoring.java index 4078bee27..c85bc8a48 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoring.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoring.java @@ -24,8 +24,12 @@ package io.jenkins.pluginhealth.scoring.scores; +import java.util.List; import java.util.Map; +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; import io.jenkins.pluginhealth.scoring.probes.DeprecatedPluginProbe; import org.springframework.stereotype.Component; @@ -36,20 +40,47 @@ public class DeprecatedPluginScoring extends Scoring { private static final String KEY = "deprecation"; @Override - public String key() { - return KEY; + public List getComponents() { + return List.of( + new ScoringComponent() { + @Override + public String getDescription() { + return "The plugin must not be marked as deprecated."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(DeprecatedPluginProbe.KEY); + if (probeResult == null) { + return new ScoringComponentResult(0, getWeight(), List.of("Cannot determine if the plugin is marked as deprecated or not.")); + } + + return switch (probeResult.message()) { + case "This plugin is marked as deprecated." -> + new ScoringComponentResult(0, getWeight(), List.of("Plugin is marked as deprecated.")); + case "This plugin is NOT deprecated." -> + new ScoringComponentResult(100, getWeight(), List.of("Plugin is not marked as deprecated.")); + default -> + new ScoringComponentResult(0, getWeight(), List.of("Cannot determine if the plugin is marked as deprecated or not.", probeResult.message())); + }; + } + + @Override + public int getWeight() { + return 1; + } + } + ); } @Override - public float coefficient() { - return COEFFICIENT; + public String key() { + return KEY; } @Override - public Map getScoreComponents() { - return Map.of( - DeprecatedPluginProbe.KEY, 1f - ); + public float weight() { + return COEFFICIENT; } @Override diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoring.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoring.java index a2163183e..59e458f67 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoring.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoring.java @@ -24,13 +24,18 @@ package io.jenkins.pluginhealth.scoring.scores; +import java.util.List; import java.util.Map; +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; import io.jenkins.pluginhealth.scoring.probes.ContinuousDeliveryProbe; import io.jenkins.pluginhealth.scoring.probes.DependabotProbe; import io.jenkins.pluginhealth.scoring.probes.DependabotPullRequestProbe; import io.jenkins.pluginhealth.scoring.probes.DocumentationMigrationProbe; import io.jenkins.pluginhealth.scoring.probes.JenkinsfileProbe; +import io.jenkins.pluginhealth.scoring.probes.RenovateProbe; import org.springframework.stereotype.Component; @@ -40,24 +45,151 @@ public class PluginMaintenanceScoring extends Scoring { private static final String KEY = "repository-configuration"; @Override - public String key() { - return KEY; + public List getComponents() { + return List.of( + new ScoringComponent() { // JenkinsFile presence + @Override + public String getDescription() { + return "Plugin must have a Jenkinsfile."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(JenkinsfileProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the presence of Jenkinsfile.")); + } + return switch (probeResult.message()) { + case "Jenkinsfile found" -> + new ScoringComponentResult(100, getWeight(), List.of("Jenkinsfile detected in plugin repository.")); + case "No Jenkinsfile found" -> + new ScoringComponentResult(0, getWeight(), List.of("Jenkinsfile not detected in plugin repository.")); + default -> + new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the presence of Jenkinsfile.", probeResult.message())); + }; + } + + @Override + public int getWeight() { + return 65; + } + }, + new ScoringComponent() { // Documentation migration done + @Override + public String getDescription() { + return "Plugin documentation should be migrated from the wiki."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(DocumentationMigrationProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the documentation migration.")); + } + return switch (probeResult.message()) { + case "Documentation is located in the plugin repository." -> + new ScoringComponentResult(100, getWeight(), List.of("Documentation is in plugin repository.")); + case "Documentation is not located in the plugin repository." -> + new ScoringComponentResult(0, getWeight(), List.of("Documentation should be migrated in plugin repository.")); + default -> + new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the documentation migration.", probeResult.message())); + }; + } + + @Override + public int getWeight() { + return 15; + } + }, + new ScoringComponent() { // Dependabot and not dependency pull requests + @Override + public String getDescription() { + return "Plugin should be using a using a dependency version management bot."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult dependabot = probeResults.get(DependabotProbe.KEY); + final ProbeResult renovate = probeResults.get(RenovateProbe.KEY); + final ProbeResult dependencyPullRequest = probeResults.get(DependabotPullRequestProbe.KEY); + + if (dependabot != null && "dependabot is configured.".equals(dependabot.message()) && renovate != null && "renovate is configured.".equals(renovate.message())) { + return new ScoringComponentResult(50, getWeight(), List.of("It seems that both dependabot and renovate are configured.", dependabot.message(), renovate.message())); + } + + if (dependabot != null && ProbeResult.Status.SUCCESS.equals(dependabot.status()) && "dependabot is configured.".equals(dependabot.message())) { + return manageOpenDependencyPullRequestValue(dependabot, dependencyPullRequest); + } + if (renovate != null && ProbeResult.Status.SUCCESS.equals(renovate.status()) && "renovate is configured.".equals(renovate.message())) { + return manageOpenDependencyPullRequestValue(renovate, dependencyPullRequest); + } + + return new ScoringComponentResult(0, getWeight(), List.of("No dependency version manager bot are used on the plugin repository.")); + } + + private ScoringComponentResult manageOpenDependencyPullRequestValue(ProbeResult dependencyBotResult, ProbeResult dependencyPullRequestResult) { + if (dependencyPullRequestResult != null && "0".equals(dependencyPullRequestResult.message())) { + return new ScoringComponentResult( + 100, + getWeight(), + List.of(dependencyBotResult.message(), "%s open dependency pull request".formatted(dependencyPullRequestResult.message())) + ); + } + return new ScoringComponentResult( + 0, + getWeight(), + List.of( + dependencyBotResult.message(), + dependencyPullRequestResult == null ? + "Cannot determine if there is any dependency pull request opened on the repository." : + "%s open dependency pull requests".formatted(dependencyPullRequestResult.message()) + ) + ); + } + + @Override + public int getWeight() { + return 15; + } + }, + new ScoringComponent() { // ContinuousDelivery JEP + @Override + public String getDescription() { + return "The plugin could benefit from setting up the continuous delivery workflow."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(ContinuousDeliveryProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the JEP-229 configuration.")); + } + return switch (probeResult.message()) { + case "JEP-229 workflow definition found." -> + new ScoringComponentResult(100, getWeight(), List.of("JEP-229 is configured on the plugin.")); + case "Could not find JEP-229 workflow definition." -> + new ScoringComponentResult(0, getWeight(), List.of("JEP-229 is not configured on the plugin.")); + default -> + new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the JEP-229 configuration.", probeResult.message())); + }; + } + + @Override + public int getWeight() { + return 5; + } + } + ); } @Override - public float coefficient() { - return COEFFICIENT; + public String key() { + return KEY; } @Override - public Map getScoreComponents() { - return Map.of( - JenkinsfileProbe.KEY, .65f, - DocumentationMigrationProbe.KEY, .15f, - DependabotProbe.KEY, .15f, - DependabotPullRequestProbe.KEY, -.15f, - ContinuousDeliveryProbe.KEY, .05f - ); + public float weight() { + return COEFFICIENT; } @Override diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/Scoring.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/Scoring.java index 8c78781f9..e8c21cec7 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/Scoring.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/Scoring.java @@ -24,12 +24,19 @@ package io.jenkins.pluginhealth.scoring.scores; -import java.util.Map; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.DoubleStream; import io.jenkins.pluginhealth.scoring.model.Plugin; -import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.ScoreResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; /** * Represents a scoring process of a plugin, based on ProbeResults contained within the Plugin#details map. @@ -37,35 +44,51 @@ public abstract class Scoring { /** * Starts the scoring process of the plugin. - * By default, the method is using the {@link Scoring#getScoreComponents()} map to compute the score based on the - * {@link Plugin#getDetails()} map. For each score component, if the probe is present in the plugin details and is - * successful, the score get the maximum score for that component. + * At the end of the process, a {@link ScoreResult} instance must be returned, describing the score of the plugin and its reasons. * * @param plugin the plugin to score * @return a {@link ScoreResult} describing the plugin based on the ProbeResult and the scoring requirements. */ - public ScoreResult apply(Plugin plugin) { - final Map scoreComponents = this.getScoreComponents(); - float max = 0; - float score = 0; + public final ScoreResult apply(Plugin plugin) { + return getComponents().stream() + .map(changelog -> changelog.getScore(plugin, plugin.getDetails())) + .collect(new Collector, ScoreResult>() { + @Override + public Supplier> supplier() { + return HashSet::new; + } + + @Override + public BiConsumer, ScoringComponentResult> accumulator() { + return Set::add; + } - for (Map.Entry component : scoreComponents.entrySet()) { - final float componentMaxValue = component.getValue(); - if (componentMaxValue > 0) { - max += componentMaxValue; - } - final ProbeResult probeResult = plugin.getDetails().get(component.getKey()); - if (probeResult != null) { - if (componentMaxValue > 0 && probeResult.status().equals(ResultStatus.SUCCESS)) { - score += componentMaxValue; - } else if (probeResult.status().equals(ResultStatus.FAILURE) && componentMaxValue < 0) { - score += componentMaxValue; + @Override + public BinaryOperator> combiner() { + return (changelogResults, changelogResults2) -> { + changelogResults.addAll(changelogResults2); + return changelogResults; + }; } - } - } - score = Math.round(Math.max(score, 0) / max * 100) / 100f; - return new ScoreResult(key(), score, coefficient()); + @Override + public Function, ScoreResult> finisher() { + return changelogResults -> { + final double sum = changelogResults.stream() + .flatMapToDouble(changelogResult -> DoubleStream.of(changelogResult.score() * changelogResult.weight())) + .sum(); + final double weight = changelogResults.stream() + .flatMapToDouble(changelogResult -> DoubleStream.of(changelogResult.weight())) + .sum(); + return new ScoreResult(key(), (int) Math.max(0, Math.round(sum / weight)), weight(), changelogResults); + }; + } + + @Override + public Set characteristics() { + return Set.of(Characteristics.UNORDERED); + } + }); } /** @@ -81,27 +104,23 @@ public ScoreResult apply(Plugin plugin) { * * @return the weight of the scoring implementation. */ - public abstract float coefficient(); + public abstract float weight(); /** - * Returns a map describing the probe required for the score computation and their maximum score. - *
- * The key is the probe key to consider. - * The value is the value to be added to the score if the probe result is successful. - * The value can be negative and in that case, the value will be considered in case the probe result is not + * Returns a description of the scoring implementation. * - * @return map of Probe key to evaluate and their maximum score + * @return the description of the scoring implementation */ - public abstract Map getScoreComponents(); + public abstract String description(); /** - * Returns a description of the scoring implementation. + * Provides the list of elements evaluated for this scoring. * - * @return the description of the scoring implementation + * @return the list of {@link ScoringComponent} considered for this score category of a plugin. */ - public abstract String description(); + public abstract List getComponents(); - public String name() { + public final String name() { return getClass().getSimpleName(); } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/ScoringComponent.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/ScoringComponent.java new file mode 100644 index 000000000..1d1412842 --- /dev/null +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/ScoringComponent.java @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.scores; + +import java.util.Map; + +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; + +public interface ScoringComponent { + /** + * Provides a human readable description of the behavior of the Changelog. + * + * @return the description of the implementation. + */ + String getDescription(); + + /** + * Evaluates the provided {@link ProbeResult} map against the requirement for this implementation + * and returns a {@link ScoringComponentResult} describing the evaluation. + * + * @param plugin the plugin to score + * @param probeResults the plugin's {@link ProbeResult} map + * @return a {@link ScoringComponentResult} describing the evaluation done based on the provided {@link ProbeResult} map + */ + ScoringComponentResult getScore(Plugin plugin, Map probeResults); + + /** + * The weight of this Changelog + * + * @return an integer representing the importance of the current Changelog implementation. + */ + int getWeight(); +} diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoring.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoring.java index de6c94c06..7ea455113 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoring.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoring.java @@ -24,8 +24,12 @@ package io.jenkins.pluginhealth.scoring.scores; +import java.util.List; import java.util.Map; +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; import io.jenkins.pluginhealth.scoring.probes.KnownSecurityVulnerabilityProbe; import org.springframework.stereotype.Component; @@ -36,18 +40,42 @@ public class SecurityWarningScoring extends Scoring { private static final String KEY = "security"; @Override - public String key() { - return KEY; + public List getComponents() { + return List.of( + new ScoringComponent() { + @Override + public String getDescription() { + return "The plugin must not have on-going security advisory."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(KnownSecurityVulnerabilityProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine if plugin has on-going security advisory.")); + } + if ("No known security vulnerabilities.".equals(probeResult.message())) { + return new ScoringComponentResult(100, getWeight(), List.of("Plugin does not seem to have on-going security advisory.")); + } + return new ScoringComponentResult(0, getWeight(), List.of("Plugin seem to have on-going security advisory.", probeResult.message())); + } + + @Override + public int getWeight() { + return 1; + } + } + ); } @Override - public float coefficient() { - return COEFFICIENT; + public String key() { + return KEY; } @Override - public Map getScoreComponents() { - return Map.of(KnownSecurityVulnerabilityProbe.KEY, 1f); + public float weight() { + return COEFFICIENT; } @Override diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoring.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoring.java index 0f226cf37..0f252eeae 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoring.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoring.java @@ -24,8 +24,12 @@ package io.jenkins.pluginhealth.scoring.scores; +import java.util.List; import java.util.Map; +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; import io.jenkins.pluginhealth.scoring.probes.UpdateCenterPluginPublicationProbe; import org.springframework.stereotype.Component; @@ -36,18 +40,47 @@ public class UpdateCenterPublishedPluginDetectionScoring extends Scoring { public static final String KEY = "update-center-plugin-publication"; @Override - public String key() { - return KEY; + public List getComponents() { + return List.of( + new ScoringComponent() { + @Override + public String getDescription() { + return "Plugin should be present in the update-center to be distributed."; + } + + @Override + public ScoringComponentResult getScore(Plugin $, Map probeResults) { + final ProbeResult probeResult = probeResults.get(UpdateCenterPluginPublicationProbe.KEY); + if (probeResult == null || ProbeResult.Status.ERROR.equals(probeResult.status())) { + return new ScoringComponentResult(-100, 100, List.of("Cannot determine if the plugin is part of the update-center.")); + } + + return switch (probeResult.message()) { + case "This plugin is still actively published by the update-center." -> + new ScoringComponentResult(100, getWeight(), List.of("The plugin appears in the update-center.")); + case "This plugin's publication has been stopped by the update-center." -> + new ScoringComponentResult(0, getWeight(), List.of("Ths plugin is not part of the update-center.")); + default -> + new ScoringComponentResult(-5, getWeight(), List.of("Cannot determine if the plugin is part of the update-center or not.", probeResult.message())); + }; + } + + @Override + public int getWeight() { + return 1; + } + } + ); } @Override - public float coefficient() { - return COEFFICIENT; + public String key() { + return KEY; } @Override - public Map getScoreComponents() { - return Map.of(UpdateCenterPluginPublicationProbe.KEY, 1f); + public float weight() { + return COEFFICIENT; } @Override diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java index b1ccb5d5a..ef0e92301 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java @@ -86,7 +86,7 @@ public int deleteOldScores() { return repository.deleteOldScoreFromPlugin(); } - public record ScoreStatistics(int average, int minimum, int maximum, int firstQuartile, int median, + public record ScoreStatistics(double average, int minimum, int maximum, int firstQuartile, int median, int thirdQuartile) { } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/model/PluginTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/model/PluginTest.java index 66b3b5108..a8b69d1e0 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/model/PluginTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/model/PluginTest.java @@ -39,7 +39,7 @@ class PluginTest { @Test void shouldUpdateDetailsIfNoPreviousDetailsPresent() { final Plugin plugin = spy(Plugin.class); - final ProbeResult probeResult = ProbeResult.success("foo", "this is a message"); + final ProbeResult probeResult = ProbeResult.success("foo", "this is a message", 1); plugin.addDetails(probeResult); assertThat(plugin.getDetails()).hasSize(1); @@ -52,8 +52,8 @@ void shouldUpdateDetailsIfPreviousDetailsPresentAndDifferent() { final String probeKey = "foo"; final String probeResultMessage = "this is a message"; - final ProbeResult previousProbeResult = new ProbeResult(probeKey, probeResultMessage, ResultStatus.FAILURE, ZonedDateTime.now().minusMinutes(10)); - final ProbeResult probeResult = new ProbeResult(probeKey, probeResultMessage, ResultStatus.SUCCESS, ZonedDateTime.now()); + final ProbeResult previousProbeResult = new ProbeResult(probeKey, probeResultMessage, ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusMinutes(10), 1); + final ProbeResult probeResult = new ProbeResult(probeKey, probeResultMessage, ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 1); plugin.addDetails(previousProbeResult); plugin.addDetails(probeResult); @@ -69,8 +69,8 @@ void shouldNotUpdateDetailsIfPreviousDetailsPresentButSame() { final String probeKey = "foo"; final String probeResultMessage = "this is a message"; - final ProbeResult previousProbeResult = new ProbeResult(probeKey, probeResultMessage, ResultStatus.SUCCESS, ZonedDateTime.now().minusMinutes(10)); - final ProbeResult probeResult = new ProbeResult(probeKey, probeResultMessage, ResultStatus.SUCCESS, ZonedDateTime.now()); + final ProbeResult previousProbeResult = new ProbeResult(probeKey, probeResultMessage, ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusMinutes(10), 1); + final ProbeResult probeResult = new ProbeResult(probeKey, probeResultMessage, ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 1); plugin.addDetails(previousProbeResult); plugin.addDetails(probeResult); @@ -81,13 +81,13 @@ void shouldNotUpdateDetailsIfPreviousDetailsPresentButSame() { } @Test - void shouldOverrideSuccessfulPreviousResultWithFailure() { + void shouldOverrideSuccessfulPreviousResultWithSUCCESS() { final Plugin plugin = spy(Plugin.class); final String probeKey = "foo"; final String probeResultMessage = "this is a message"; - final ProbeResult previousProbeResult = new ProbeResult(probeKey, probeResultMessage, ResultStatus.SUCCESS, ZonedDateTime.now().minusMinutes(10)); - final ProbeResult probeResult = new ProbeResult(probeKey, probeResultMessage, ResultStatus.FAILURE, ZonedDateTime.now()); + final ProbeResult previousProbeResult = new ProbeResult(probeKey, probeResultMessage, ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusMinutes(10), 1); + final ProbeResult probeResult = new ProbeResult(probeKey, probeResultMessage, ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 1); plugin.addDetails(previousProbeResult); plugin.addDetails(probeResult); @@ -102,10 +102,10 @@ void shouldRemoveEntryWhenNewStatusInError() { final Plugin plugin = spy(Plugin.class); final String probeKey = "foo"; - plugin.addDetails(ProbeResult.success(probeKey, "")); + plugin.addDetails(ProbeResult.success(probeKey, "", 1)); assertThat(plugin.getDetails()).hasSize(1); - plugin.addDetails(ProbeResult.error(probeKey, "")); + plugin.addDetails(ProbeResult.error(probeKey, "", 1)); assertThat(plugin.getDetails()).isEmpty(); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ProbeResultTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ProbeResultTest.java index cfa7a0323..064ccca57 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ProbeResultTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ProbeResultTest.java @@ -33,32 +33,40 @@ class ProbeResultTest { @Test void shouldBeEqualsWithSameStatusAndIdAndMessage() { - final ProbeResult result1 = new ProbeResult("probe", "this is a message", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)); - final ProbeResult result2 = new ProbeResult("probe", "this is a message", ResultStatus.SUCCESS, ZonedDateTime.now()); + final ProbeResult result1 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1); + final ProbeResult result2 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 1); assertThat(result1).isEqualTo(result2); } @Test void shouldNotBeEqualsWithDifferentMessages() { - final ProbeResult result1 = new ProbeResult("probe", "this is a message", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)); - final ProbeResult result2 = new ProbeResult("probe", "this is a different message", ResultStatus.SUCCESS, ZonedDateTime.now()); + final ProbeResult result1 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1); + final ProbeResult result2 = new ProbeResult("probe", "this is a different message", ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 1); assertThat(result1).isNotEqualTo(result2); } @Test void shouldNotBeEqualsWithDifferentStatues() { - final ProbeResult result1 = new ProbeResult("probe", "this is a message", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)); - final ProbeResult result2 = new ProbeResult("probe", "this is a message", ResultStatus.FAILURE, ZonedDateTime.now()); + final ProbeResult result1 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1); + final ProbeResult result2 = new ProbeResult("probe", "this is a message", ProbeResult.Status.ERROR, ZonedDateTime.now(), 1); assertThat(result1).isNotEqualTo(result2); } @Test void shouldNotBeEqualsWithDifferentMessagesAndStatues() { - final ProbeResult result1 = new ProbeResult("probe", "this is a message", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)); - final ProbeResult result2 = new ProbeResult("probe", "this is a different message", ResultStatus.FAILURE, ZonedDateTime.now()); + final ProbeResult result1 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1); + final ProbeResult result2 = new ProbeResult("probe", "this is a different message", ProbeResult.Status.ERROR, ZonedDateTime.now(), 1); + + assertThat(result1).isNotEqualTo(result2); + } + + @Test + void shouldNotBeEqualWithDifferentVersions() { + final ProbeResult result1 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 1); + final ProbeResult result2 = new ProbeResult("probe", "this is a message", ProbeResult.Status.SUCCESS, ZonedDateTime.now(), 2); assertThat(result1).isNotEqualTo(result2); } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ScoreTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ScoreTest.java index acb2db555..a9ad5061b 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ScoreTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/model/ScoreTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock; import java.time.ZonedDateTime; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,15 +43,15 @@ void shouldBeAbleToAdjustScoreValueWithNewDetails() { assertThat(score.getValue()).isEqualTo(0); - score.addDetail(new ScoreResult("foo", 1, .4f)); + score.addDetail(new ScoreResult("foo", 100, .4f, Set.of())); assertThat(score.getDetails().size()).isEqualTo(1); assertThat(score.getValue()).isEqualTo(100); - score.addDetail(new ScoreResult("bar", 0, .2f)); + score.addDetail(new ScoreResult("bar", 0, .2f, Set.of())); assertThat(score.getDetails().size()).isEqualTo(2); assertThat(score.getValue()).isEqualTo(67); - score.addDetail(new ScoreResult("wiz", 1, .3f)); + score.addDetail(new ScoreResult("wiz", 100, .3f, Set.of())); assertThat(score.getDetails().size()).isEqualTo(3); assertThat(score.getValue()).isEqualTo(78); } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/AbstractProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/AbstractProbeTest.java index 8cce6daed..a521f68e1 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/AbstractProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/AbstractProbeTest.java @@ -45,9 +45,4 @@ void shouldUseValidKey() { void shouldHaveDescription() { assertThat(getSpy().getDescription()).isNotBlank(); } - - @Test - void shouldNotHaveNullRequirement() { - assertThat(getSpy().getProbeResultRequirement()).isNotNull(); - } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java index 540d488ce..c828f7287 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java @@ -26,7 +26,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -64,134 +63,42 @@ public void shouldBeRelatedToCode() { assertThat(getSpy().isSourceCodeRelated()).isTrue(); } - @Test - public void shouldBeInErrorWhenSCMWasNotValidated() { - final String pluginName = "foo"; - final String scmLink = "foo-bar"; - - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - - when(plugin.getName()).thenReturn(pluginName); - when(plugin.getScm()).thenReturn(scmLink); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - - final CodeCoverageProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, ctx); - - verify(probe, never()).doApply(plugin, ctx); - assertThat(result) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(CodeCoverageProbe.KEY, "")); - } - @Test public void shouldBeInErrorWhenRepositoryIsNotInOrganization() { final String pluginName = "foo"; final String scmLink = "foo-bar"; - final String defaultBranch = "main"; final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - final GitHub gh = mock(GitHub.class); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - - when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( - pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, - "42", defaultBranch - ) - ), - Map.of(), - List.of() - )); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.empty()); - when(ctx.getGitHub()).thenReturn(gh); - - final CodeCoverageProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(CodeCoverageProbe.KEY, "")); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldBeSuccessfulWhenRetrievedDetailsFromGitHubChecksIsAboveMinimum() throws IOException { - final String pluginName = "mailer"; - final String pluginRepo = "jenkinsci/" + pluginName + "-plugin"; - final String scmLink = "https://github.com/" + pluginRepo; - final String defaultBranch = "main"; - - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - - final GitHub gh = mock(GitHub.class); - final GHRepository ghRepository = mock(GHRepository.class); - when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, - "42", defaultBranch + "42", "main" ) ), Map.of(), List.of() )); - when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); - - when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); - final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); - final GHCheckRun checkRun = mock(GHCheckRun.class); - final GHCheckRun.Output output = mock(GHCheckRun.Output.class); - when(output.getTitle()).thenReturn("Line: 84.95% (+0.00% against last successful build). Branch: 76.52% (+0.00% against last successful build)."); - when(checkRun.getOutput()).thenReturn(output); - when(checkRuns.toList()).thenReturn( - List.of(checkRun) - ); - when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))) - .thenReturn(checkRuns); + when(ctx.getScmRepository()).thenReturn(Optional.empty()); + when(ctx.getRepositoryName()).thenReturn(Optional.empty()); final CodeCoverageProbe probe = getSpy(); final ProbeResult result = probe.apply(plugin, ctx); - verify(probe).doApply(plugin, ctx); assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(CodeCoverageProbe.KEY, "Line coverage is above 70%. Branch coverage is above 60%.")); + .isEqualTo(ProbeResult.error(CodeCoverageProbe.KEY, "Cannot determine plugin repository.", probe.getVersion())); } @SuppressWarnings("unchecked") @Test - public void shouldBeSuccessfulWhenAboveThreshold() throws IOException { + public void shouldSucceedToRetrieveCoverageData() throws IOException { final String pluginName = "mailer"; final String pluginRepo = "jenkinsci/" + pluginName + "-plugin"; final String scmLink = "https://github.com/" + pluginRepo; @@ -204,12 +111,6 @@ public void shouldBeSuccessfulWhenAboveThreshold() throws IOException { final GHRepository ghRepository = mock(GHRepository.class); when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( @@ -222,76 +123,19 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 List.of() )); when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); + when(ctx.getRepositoryName()).thenReturn(Optional.of(pluginRepo)); when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); - final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); - final GHCheckRun checkRun = mock(GHCheckRun.class); - final GHCheckRun.Output output = mock(GHCheckRun.Output.class); - when(output.getTitle()).thenReturn("Line Coverage: 70.56% (+0.00%), Branch Coverage: 63.37% (+0.00%)"); - when(checkRun.getOutput()).thenReturn(output); - when(checkRuns.toList()).thenReturn( - List.of(checkRun) - ); - when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))) - .thenReturn(checkRuns); - - final CodeCoverageProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, ctx); - - verify(probe).doApply(plugin, ctx); - assertThat(result) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(CodeCoverageProbe.KEY, "Line coverage is above 70%. Branch coverage is above 60%.")); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnBothCriteria() throws IOException { - final String pluginName = "mailer"; - final String pluginRepo = "jenkinsci/" + pluginName + "-plugin"; - final String scmLink = "https://github.com/" + pluginRepo; - final String defaultBranch = "main"; - - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - - final GitHub gh = mock(GitHub.class); - final GHRepository ghRepository = mock(GHRepository.class); - - when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(plugin.getScm()).thenReturn(scmLink); - when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( - pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, - "42", defaultBranch - ) - ), - Map.of(), - List.of() - )); - when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); - when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); final GHCheckRun checkRun = mock(GHCheckRun.class); final GHCheckRun.Output output = mock(GHCheckRun.Output.class); - when(output.getTitle()).thenReturn("Line: 51.42% (+0.00% against last successful build). Branch: 44.52% (+0.00% against last successful build)."); + when(output.getTitle()).thenReturn("Line Coverage: 70.56% (+0.00%), Branch Coverage: 63.37% (+0.00%)"); when(checkRun.getOutput()).thenReturn(output); when(checkRuns.toList()).thenReturn( List.of(checkRun) ); - when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))) - .thenReturn(checkRuns); + when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))).thenReturn(checkRuns); final CodeCoverageProbe probe = getSpy(); final ProbeResult result = probe.apply(plugin, ctx); @@ -300,12 +144,11 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(CodeCoverageProbe.KEY, "Line coverage is below 70%. Branch coverage is below 60%.")); + .isEqualTo(ProbeResult.success(CodeCoverageProbe.KEY, "Line coverage: 70.56%. Branch coverage: 63.37%.", probe.getVersion())); } - @SuppressWarnings("unchecked") @Test - public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnBranch() throws IOException { + void shouldStillBeSuccessfulWhenNoCoverageCanBeFound() throws IOException { final String pluginName = "mailer"; final String pluginRepo = "jenkinsci/" + pluginName + "-plugin"; final String scmLink = "https://github.com/" + pluginRepo; @@ -318,12 +161,6 @@ public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnBranch final GHRepository ghRepository = mock(GHRepository.class); when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( @@ -336,76 +173,13 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 List.of() )); when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); + when(ctx.getRepositoryName()).thenReturn(Optional.of(pluginRepo)); when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); - final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); - final GHCheckRun checkRun = mock(GHCheckRun.class); - final GHCheckRun.Output output = mock(GHCheckRun.Output.class); - when(output.getTitle()).thenReturn("Line: 81.95% (+0.00% against last successful build). Branch: 54.52% (+0.00% against last successful build)."); - when(checkRun.getOutput()).thenReturn(output); - when(checkRuns.toList()).thenReturn( - List.of(checkRun) - ); - when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))) - .thenReturn(checkRuns); - - final CodeCoverageProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, ctx); - - verify(probe).doApply(plugin, ctx); - assertThat(result) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(CodeCoverageProbe.KEY, "Line coverage is above 70%. Branch coverage is below 60%.")); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnLine() throws IOException { - final String pluginName = "mailer"; - final String pluginRepo = "jenkinsci/" + pluginName + "-plugin"; - final String scmLink = "https://github.com/" + pluginRepo; - final String defaultBranch = "main"; - - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - - final GitHub gh = mock(GitHub.class); - final GHRepository ghRepository = mock(GHRepository.class); - - when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(plugin.getScm()).thenReturn(scmLink); - when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( - pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, - "42", defaultBranch - ) - ), - Map.of(), - List.of() - )); - when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); - when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); - final GHCheckRun checkRun = mock(GHCheckRun.class); - final GHCheckRun.Output output = mock(GHCheckRun.Output.class); - when(output.getTitle()).thenReturn("Line: 61.95% (+0.00% against last successful build). Branch: 84.52% (+0.00% against last successful build)."); - when(checkRun.getOutput()).thenReturn(output); - when(checkRuns.toList()).thenReturn( - List.of(checkRun) - ); - when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))) - .thenReturn(checkRuns); + when(checkRuns.toList()).thenReturn(List.of()); + when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))).thenReturn(checkRuns); final CodeCoverageProbe probe = getSpy(); final ProbeResult result = probe.apply(plugin, ctx); @@ -414,56 +188,10 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(CodeCoverageProbe.KEY, "Line coverage is below 70%. Branch coverage is above 60%.")); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldBeInErrorIfThereIsNoCodeCoverage() throws IOException { - final String pluginName = "mailer"; - final String pluginRepo = "jenkinsci/" + pluginName + "-plugin"; - final String scmLink = "https://github.com/" + pluginRepo; - final String defaultBranch = "main"; - - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - - final GitHub gh = mock(GitHub.class); - final GHRepository ghRepository = mock(GHRepository.class); - - when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(plugin.getScm()).thenReturn(scmLink); - when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( - pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, - "42", defaultBranch - ) - ), - Map.of(), - List.of() - )); - when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); - - when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); - final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); - when(ghRepository.getCheckRuns(defaultBranch, Map.of("check_name", "Code Coverage"))) - .thenReturn(checkRuns); - - final CodeCoverageProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, ctx); - - verify(probe).doApply(plugin, ctx); - assertThat(result) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(CodeCoverageProbe.KEY, "")); + .isEqualTo(ProbeResult.success( + CodeCoverageProbe.KEY, + "Could not determine code coverage for the plugin.", + probe.getVersion() + )); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbeTest.java index 3fb49f2d6..4df4a816e 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContinuousDeliveryProbeTest.java @@ -32,11 +32,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Map; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import org.junit.jupiter.api.Test; @@ -62,15 +61,13 @@ void shouldBeAbleToDetectRepositoryWithNoGHA() throws Exception { final ProbeContext ctx = mock(ProbeContext.class); final ContinuousDeliveryProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(Files.createTempDirectory("foo")); + final Path repo = Files.createTempDirectory("foo"); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("Plugin has no GitHub Action configured"); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "Plugin has no GitHub Action configured.", probe.getVersion())); } @Test @@ -79,17 +76,14 @@ void shouldBeAbleToDetectNotConfiguredRepository() throws Exception { final ProbeContext ctx = mock(ProbeContext.class); final ContinuousDeliveryProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); Files.createDirectories(repo.resolve(".github/workflows")); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("Could not find JEP-229 workflow definition"); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", probe.getVersion())); } @Test @@ -97,12 +91,8 @@ void shouldBeAbleToDetectConfiguredRepository() throws Exception { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); final Path workflows = Files.createDirectories(repo.resolve(".github/workflows")); final Path cdWorkflowDef = Files.createFile(workflows.resolve("continuous-delivery.yml")); @@ -120,7 +110,7 @@ void shouldBeAbleToDetectConfiguredRepository() throws Exception { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found")); + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", probe.getVersion())); } @Test @@ -128,12 +118,8 @@ void shouldBeAbleToDetectConfiguredRepositoryWithLongExtension() throws Exceptio final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); final Path workflows = Files.createDirectories(repo.resolve(".github/workflows")); final Path cdWorkflowDef = Files.createFile(workflows.resolve("continuous-delivery.yml")); @@ -151,20 +137,16 @@ void shouldBeAbleToDetectConfiguredRepositoryWithLongExtension() throws Exceptio assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found")); + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", probe.getVersion())); } @Test - void shouldNotBeAbleToFindWorkflowDefinitionBasedOnFilename() throws Exception { + void shouldNotBeAbleToFindWorkflowDefinitionBasedOnlyOnFilename() throws Exception { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); final Path workflows = Files.createDirectories(repo.resolve(".github/workflows")); final Path cdWorkflowDef = Files.createFile(workflows.resolve("cd.yml")); @@ -182,7 +164,7 @@ void shouldNotBeAbleToFindWorkflowDefinitionBasedOnFilename() throws Exception { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition")); + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", probe.getVersion())); } @Test @@ -190,12 +172,8 @@ void shouldBeAbleToSurviveIncompleteWorkflowDefinition() throws Exception { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); final Path workflows = Files.createDirectories(repo.resolve(".github/workflows")); final Path cdWorkflowDef = Files.createFile(workflows.resolve("cd.yml")); @@ -210,7 +188,7 @@ void shouldBeAbleToSurviveIncompleteWorkflowDefinition() throws Exception { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition")); + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", probe.getVersion())); } @Test @@ -218,12 +196,8 @@ void shouldBeAbleToSurviveInvalidWorkflowDefinition() throws Exception { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); final Path workflows = Files.createDirectories(repo.resolve(".github/workflows")); final Path cdWorkflowDef = Files.createFile(workflows.resolve("cd.yml")); @@ -243,6 +217,6 @@ void shouldBeAbleToSurviveInvalidWorkflowDefinition() throws Exception { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition")); + .isEqualTo(ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbeTest.java index 92296016e..1b37f7341 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/ContributingGuidelinesProbeTest.java @@ -32,11 +32,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import org.junit.jupiter.api.Test; @@ -63,14 +62,13 @@ public void shouldCorrectlyDetectMissingContributingGuidelines() throws IOExcept final ContributingGuidelinesProbe probe = getSpy(); when(plugin.getName()).thenReturn("foo"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repository = Files.createTempDirectory(plugin.getName()); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); - assertThat(probe.apply(plugin, ctx).status()).isEqualTo(ResultStatus.FAILURE); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(ContributingGuidelinesProbe.KEY, "No contributing guidelines found.", probe.getVersion())); } @Test @@ -80,16 +78,14 @@ public void shouldCorrectlyDetectContributingGuidelinesInRootLevelOfRepository() final ContributingGuidelinesProbe probe = getSpy(); when(plugin.getName()).thenReturn("foo"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repository = Files.createTempDirectory(plugin.getName()); Files.createFile(repository.resolve("CONTRIBUTING.md")); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(ContributingGuidelinesProbe.KEY, "Contributing guidelines found.", probe.getVersion())); } @Test @@ -99,15 +95,13 @@ public void shouldCorrectlyDetectContributingGuidelinesInDocsFolder() throws IOE final ContributingGuidelinesProbe probe = getSpy(); when(plugin.getName()).thenReturn("foo"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repository = Files.createTempDirectory(plugin.getName()); Files.createFile(Files.createDirectory(repository.resolve("docs")).resolve("CONTRIBUTING.md")); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(ContributingGuidelinesProbe.KEY, "Contributing guidelines found.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbeTest.java index 26e92f98c..8a6835cd3 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotProbeTest.java @@ -27,14 +27,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; @@ -63,36 +62,15 @@ void shouldRequireValidSCMAndLastCommit() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, "") - ), - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - ), - Map.of( - LastCommitDateProbe.KEY, ProbeResult.failure(LastCommitDateProbe.KEY, "") - ), - Map.of( - LastCommitDateProbe.KEY, ProbeResult.failure(LastCommitDateProbe.KEY, ""), - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - ), - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.failure(LastCommitDateProbe.KEY, "") - ) - ); + when(plugin.getName()).thenReturn("foo"); + when(ctx.getScmRepository()).thenReturn(Optional.empty()); final DependabotProbe probe = getSpy(); - for (int i = 0; i < 6; i++) { - assertThat(probe.apply(plugin, ctx)) - .usingRecursiveComparison() - .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.error(DependabotProbe.KEY, "dependabot does not meet the criteria to be executed on null")); - verify(probe, never()).doApply(plugin, ctx); - } + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "message", "status") + .isEqualTo(ProbeResult.error(DependabotProbe.KEY, "There is no local repository for plugin " + plugin.getName() + ".", probe.getVersion())); } @Test @@ -101,17 +79,13 @@ void shouldDetectMissingDependabotFile() throws Exception { final ProbeContext ctx = mock(ProbeContext.class); final DependabotProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(DependabotProbe.KEY, "No GitHub configuration folder found")); + .isEqualTo(ProbeResult.success(DependabotProbe.KEY, "No GitHub configuration folder found.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } @@ -121,21 +95,16 @@ void shouldDetectDependabotFile() throws Exception { final ProbeContext ctx = mock(ProbeContext.class); final DependabotProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); final Path github = Files.createDirectories(repo.resolve(".github")); Files.createFile(github.resolve("dependabot.yml")); - - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.success(DependabotProbe.KEY, "dependabot is configured")); + .isEqualTo(ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbeTest.java index 0772144df..98776f79d 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DependabotPullRequestProbeTest.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; @@ -67,19 +66,14 @@ void shouldBeSkippedWhenNoDependabot() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of(DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, "")) - ); - final DependabotPullRequestProbe probe = getSpy(); assertThat(probe.apply(plugin, ctx)).usingRecursiveComparison() .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(DependabotPullRequestProbe.KEY, "")); + .isEqualTo(ProbeResult.error(DependabotPullRequestProbe.KEY, "", probe.getVersion())); assertThat(probe.apply(plugin, ctx)).usingRecursiveComparison() .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(DependabotPullRequestProbe.KEY, "")); + .isEqualTo(ProbeResult.error(DependabotPullRequestProbe.KEY, "", probe.getVersion())); } @Test @@ -90,13 +84,10 @@ void shouldAccessGitHubAPIAndSeeNoPullRequest() throws IOException { final GitHub gh = mock(GitHub.class); final GHRepository ghRepository = mock(GHRepository.class); - when(plugin.getDetails()).thenReturn(Map.of( - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "") - )); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/mailer-plugin"); when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of("jenkinsci/mailer-plugin")); + when(ctx.getRepositoryName()).thenReturn(Optional.of("jenkinsci/mailer-plugin")); when(gh.getRepository(anyString())).thenReturn(ghRepository); final GHLabel dependenciesLabel = mock(GHLabel.class); @@ -114,7 +105,7 @@ void shouldAccessGitHubAPIAndSeeNoPullRequest() throws IOException { assertThat(result).usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(DependabotPullRequestProbe.KEY, "No open pull request from dependabot")); + .isEqualTo(ProbeResult.success(DependabotPullRequestProbe.KEY, "0", probe.getVersion())); } @Test @@ -125,13 +116,10 @@ void shouldAccessGitHubAPIWhenDependabotActivatedWithOpenedPR() throws IOExcepti final GitHub gh = mock(GitHub.class); final GHRepository ghRepository = mock(GHRepository.class); - when(plugin.getDetails()).thenReturn(Map.of( - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "") - )); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/mailer-plugin"); when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of("jenkinsci/mailer-plugin")); + when(ctx.getRepositoryName()).thenReturn(Optional.of("jenkinsci/mailer-plugin")); when(gh.getRepository(anyString())).thenReturn(ghRepository); final GHLabel dependenciesLabel = mock(GHLabel.class); @@ -153,7 +141,7 @@ void shouldAccessGitHubAPIWhenDependabotActivatedWithOpenedPR() throws IOExcepti assertThat(result).usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(DependabotPullRequestProbe.KEY, "2 open pull requests from Dependabot")); + .isEqualTo(ProbeResult.success(DependabotPullRequestProbe.KEY, "2", probe.getVersion())); } @Test @@ -163,11 +151,8 @@ void shouldFailProperlyWhenIssueCommunicatingWithGitHub() throws IOException { final GitHub gh = mock(GitHub.class); - when(plugin.getDetails()).thenReturn(Map.of( - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "") - )); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/mailer-plugin"); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of("foo-bar")); + when(ctx.getRepositoryName()).thenReturn(Optional.of("foo-bar")); when(ctx.getGitHub()).thenReturn(gh); when(gh.getRepository(anyString())).thenThrow(IOException.class); @@ -178,6 +163,6 @@ void shouldFailProperlyWhenIssueCommunicatingWithGitHub() throws IOException { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.error(DependabotPullRequestProbe.KEY, "Could not count dependabot pull requests")); + .isEqualTo(ProbeResult.error(DependabotPullRequestProbe.KEY, "Could not count dependabot pull requests.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java index 94f1c6b41..8ed7e5287 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java @@ -35,7 +35,6 @@ import java.util.Map; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.Deprecation; import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; @@ -44,6 +43,7 @@ import org.junit.jupiter.api.Test; class DeprecatedPluginProbeTest extends AbstractProbeTest { + @Override DeprecatedPluginProbe getSpy() { return spy(DeprecatedPluginProbe.class); @@ -59,17 +59,20 @@ void shouldBeAbleToDetectNonDeprecatedPlugin() { final var plugin = mock(io.jenkins.pluginhealth.scoring.model.Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final DeprecatedPluginProbe probe = getSpy(); + final String pluginName = "foo"; - when(plugin.getName()).thenReturn("foo"); + when(plugin.getName()).thenReturn(pluginName); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), + Map.of(pluginName, new Plugin(pluginName, new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), Map.of("bar", new Deprecation("find-the-reason-here")), Collections.emptyList() )); - final ProbeResult result = probe.apply(plugin, ctx); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(DeprecatedPluginProbe.KEY, "This plugin is NOT deprecated.", probe.getVersion())); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); } @Test @@ -78,17 +81,18 @@ void shouldBeAbleToDetectDeprecatedPlugin() { final ProbeContext ctx = mock(ProbeContext.class); final DeprecatedPluginProbe probe = getSpy(); - when(plugin.getName()).thenReturn("foo"); + final String pluginName = "foo"; + when(plugin.getName()).thenReturn(pluginName); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), - Map.of("bar", new Deprecation("find-the-reason-here"), "foo", new Deprecation("this-is-the-reason")), + Map.of(pluginName, new Plugin(pluginName, new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), + Map.of("bar", new Deprecation("find-the-reason-here-for-plugin-bar"), pluginName, new Deprecation("this-is-the-reason")), Collections.emptyList() )); - final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("this-is-the-reason"); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(DeprecatedPluginProbe.KEY, "this-is-the-reason", probe.getVersion())); } @Test @@ -112,7 +116,7 @@ pluginName, new Plugin(pluginName, new VersionNumber("1.0"), "", ZonedDateTime.n assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(DeprecatedPluginProbe.KEY, "This plugin is marked as deprecated")); + .isEqualTo(ProbeResult.success(DeprecatedPluginProbe.KEY, "This plugin is marked as deprecated.", probe.getVersion())); } @Test @@ -129,11 +133,10 @@ void shouldSurviveIfPluginIsNotInUpdateCenter() { )); final DeprecatedPluginProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result) + assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(DeprecatedPluginProbe.KEY, "This plugin is not in update-center")); + .isEqualTo(ProbeResult.error(DeprecatedPluginProbe.KEY, "This plugin is not in update-center.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbeTest.java index c4b243d56..a988a0b0f 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DocumentationMigrationProbeTest.java @@ -54,38 +54,12 @@ void shouldNotBeRelatedToSourceCodeModifications() { @SuppressWarnings("unchecked") @Test - void shouldRequireValidSCMLink() { - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of(SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, "")) - ); - - final DocumentationMigrationProbe probe = getSpy(); - - assertThat(probe.apply(plugin, ctx)) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(DocumentationMigrationProbe.KEY, "")); - - assertThat(probe.apply(plugin, ctx)) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(DocumentationMigrationProbe.KEY, "")); - } - - @SuppressWarnings("unchecked") - @Test - void shouldNotRegisterWhenDocumentationListIfEmpty() { + void shouldNotRegisterWhenDocumentationListIsEmpty() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); when(plugin.getName()).thenReturn("foo"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); + when(plugin.getScm()).thenReturn("this-is-fine-for-now"); when(ctx.getPluginDocumentationLinks()).thenReturn( Map.of(), Map.of("something-else", "not-what-we-are-looking-for") @@ -96,12 +70,12 @@ void shouldNotRegisterWhenDocumentationListIfEmpty() { assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.error(DocumentationMigrationProbe.KEY, "No link to documentation can be confirmed")); + .isEqualTo(ProbeResult.error(DocumentationMigrationProbe.KEY, "No link to documentation can be confirmed.", probe.getVersion())); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.error(DocumentationMigrationProbe.KEY, "Plugin is not listed in documentation migration source")); + .isEqualTo(ProbeResult.error(DocumentationMigrationProbe.KEY, "Plugin is not listed in documentation migration source.", probe.getVersion())); } @Test @@ -113,9 +87,6 @@ void shouldBeAbleToDetectNotMigratedDocumentation() { when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/foo-plugin"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); when(ctx.getPluginDocumentationLinks()).thenReturn( Map.of(pluginName, "https://wiki.jenkins-ci.org/DISPLAY/foo-plugin") ); @@ -126,7 +97,7 @@ void shouldBeAbleToDetectNotMigratedDocumentation() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository")); + .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", probe.getVersion())); } @Test @@ -138,9 +109,6 @@ void shouldBeAbleToDetectMigratedDocumentation() { when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/foo-plugin"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); when(ctx.getPluginDocumentationLinks()).thenReturn( Map.of(pluginName, "https://github.com/jenkinsci/foo-plugin") ); @@ -151,7 +119,7 @@ void shouldBeAbleToDetectMigratedDocumentation() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository")); + .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", probe.getVersion())); } @Test @@ -163,9 +131,6 @@ void shouldBeAbleToAcceptExtraSlashInDocumentationLink() { when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/foo-plugin"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); when(ctx.getPluginDocumentationLinks()).thenReturn( Map.of(pluginName, "https://github.com/jenkinsci/foo-plugin/") ); @@ -176,7 +141,7 @@ void shouldBeAbleToAcceptExtraSlashInDocumentationLink() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository")); + .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", probe.getVersion())); } @Test @@ -188,9 +153,6 @@ void shouldBeAbleToAcceptTreeReference() { when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/foo-plugin"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); when(ctx.getPluginDocumentationLinks()).thenReturn( Map.of(pluginName, "https://github.com/jenkinsci/foo-plugin/tree/main") ); @@ -201,7 +163,7 @@ void shouldBeAbleToAcceptTreeReference() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository")); + .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", probe.getVersion())); } @Test @@ -213,9 +175,6 @@ void shouldBeAbleToAcceptBlobReference() { when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/foo-plugin"); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); when(ctx.getPluginDocumentationLinks()).thenReturn( Map.of(pluginName, "https://github.com/jenkinsci/foo-plugin/blob/main/this/is/documentation/README.md") ); @@ -226,6 +185,6 @@ void shouldBeAbleToAcceptBlobReference() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository")); + .isEqualTo(ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbeTest.java index def55cc53..6a43a9456 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/HasUnreleasedProductionChangesProbeTest.java @@ -27,7 +27,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,7 +36,6 @@ import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.Date; -import java.util.Map; import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; @@ -55,18 +53,24 @@ HasUnreleasedProductionChangesProbe getSpy() { } @Test - void shouldBeExecutedAfterLastCommitDateProbe() { + void shouldGenerateErrorWhenThereIsNoRepositoryInContext() { + final String pluginName = "foo"; final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - final HasUnreleasedProductionChangesProbe probe = getSpy(); - when(plugin.getName()).thenReturn("foo-bar"); + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getScmRepository()).thenReturn(Optional.empty()); + + final HasUnreleasedProductionChangesProbe probe = getSpy(); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(HasUnreleasedProductionChangesProbe.KEY, "")); - verify(probe, never()).doApply(plugin, ctx); + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error( + HasUnreleasedProductionChangesProbe.KEY, + "There is no local repository for plugin " + pluginName + ".", + probe.getVersion() + )); } @Test @@ -77,11 +81,7 @@ void shouldFailIfThereIsNotReleasedCommits() throws IOException, GitAPIException final String scmLink = "https://test-server/jenkinsci/test-repo"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getScm()).thenReturn(scmLink); final PersonIdent defaultCommitter = new PersonIdent( @@ -110,24 +110,21 @@ void shouldFailIfThereIsNotReleasedCommits() throws IOException, GitAPIException assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at pom.xml, src/main/resources/index.jelly")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at pom.xml, src/main/resources/index.jelly", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } @Test void shouldFailIfThereIsNotReleasedCommitsInModule() throws IOException, GitAPIException { final Path repository = Files.createTempDirectory("test-foo-bar"); + final Path module = Files.createDirectory(repository.resolve("test-folder")); final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final String scmLink = "https://test-server/jenkinsci/test-repo/test-folder"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); - when(ctx.getScmFolderPath()).thenReturn(Optional.of("test-folder")); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); + when(ctx.getScmFolderPath()).thenReturn(Optional.of(repository.relativize(module))); when(plugin.getScm()).thenReturn(scmLink); @@ -138,7 +135,6 @@ void shouldFailIfThereIsNotReleasedCommitsInModule() throws IOException, GitAPIE try (Git git = Git.init().setDirectory(repository.toFile()).call()) { Files.createFile(repository.resolve("pom.xml")); - final Path module = Files.createDirectory(repository.resolve("test-folder")); Files.createFile(module.resolve("pom.xml")); final Path srcMainResources = Files.createDirectories( module.resolve("src").resolve("main").resolve("resources") @@ -159,7 +155,11 @@ void shouldFailIfThereIsNotReleasedCommitsInModule() throws IOException, GitAPIE assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at pom.xml, test-folder/pom.xml, test-folder/src/main/resources/index.jelly")); + .isEqualTo(ProbeResult.success( + HasUnreleasedProductionChangesProbe.KEY, + "Unreleased production modifications might exist in the plugin source code at pom.xml, test-folder/pom.xml, test-folder/src/main/resources/index.jelly", + probe.getVersion() + )); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } @@ -172,11 +172,7 @@ void shouldSucceedWhenCommitOnPomFileBeforeLatestReleaseDate() throws IOExceptio final String pluginName = "test-plugin"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); @@ -199,7 +195,7 @@ void shouldSucceedWhenCommitOnPomFileBeforeLatestReleaseDate() throws IOExceptio assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } @@ -211,11 +207,7 @@ void shouldSucceedWhenCommitOnReadmeFileAfterReleaseDate() throws IOException, G final String scmLink = "https://test-server/jenkinsci/test-repo"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getScm()).thenReturn(scmLink); final PersonIdent defaultCommitter = new PersonIdent( @@ -236,7 +228,7 @@ void shouldSucceedWhenCommitOnReadmeFileAfterReleaseDate() throws IOException, G assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -250,12 +242,7 @@ void shouldSucceedWhenCommitOnSrcMainPathBeforeReleaseDate() throws IOException, when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); when(plugin.getScm()).thenReturn(scmLink); - - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); final PersonIdent defaultCommitter = new PersonIdent( "Not real person", "this is not a real email" @@ -277,7 +264,7 @@ void shouldSucceedWhenCommitOnSrcMainPathBeforeReleaseDate() throws IOException, assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -291,11 +278,7 @@ void shouldFailIfCommitExistsOnPomFileAfterLatestRelease() throws IOException, G final String pluginName = "test-plugin"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); @@ -317,7 +300,7 @@ void shouldFailIfCommitExistsOnPomFileAfterLatestRelease() throws IOException, G assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at pom.xml")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at pom.xml", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -331,12 +314,7 @@ void shouldFailWhenCommitOnSrcPathAfterReleaseDate() throws IOException, GitAPIE when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); when(plugin.getScm()).thenReturn(scmLink); - - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); final PersonIdent defaultCommitter = new PersonIdent( "Not real person", "this is not a real email" @@ -359,7 +337,7 @@ void shouldFailWhenCommitOnSrcPathAfterReleaseDate() throws IOException, GitAPIE assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at src/main/resources/index.jelly")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "Unreleased production modifications might exist in the plugin source code at src/main/resources/index.jelly", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -372,11 +350,7 @@ void shouldSucceedWhenCommitOnReadmeFileBeforeReleaseDate() throws IOException, final String scmLink = "https://test-server/jenkinsci/test-repo"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getScm()).thenReturn(scmLink); final PersonIdent defaultCommitter = new PersonIdent( @@ -397,7 +371,7 @@ void shouldSucceedWhenCommitOnReadmeFileBeforeReleaseDate() throws IOException, assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -411,11 +385,7 @@ void shouldSucceedWhenCommitOnTestSourcesAfterReleaseDate() throws IOException, when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); when(plugin.getScm()).thenReturn(scmLink); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); final PersonIdent defaultCommitter = new PersonIdent( "Not real person", "this is not a real email" @@ -436,7 +406,7 @@ void shouldSucceedWhenCommitOnTestSourcesAfterReleaseDate() throws IOException, assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.")); + .isEqualTo(ProbeResult.success(HasUnreleasedProductionChangesProbe.KEY, "All production modifications were released.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -450,11 +420,7 @@ void shouldBeAbleToDifferentiateFilesWithCommitsBeforeAndAfterReleaseDate() thro final String pluginName = "test-plugin"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); @@ -484,10 +450,10 @@ void shouldBeAbleToDifferentiateFilesWithCommitsBeforeAndAfterReleaseDate() thro assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure( + .isEqualTo(ProbeResult.success( HasUnreleasedProductionChangesProbe.KEY, - "Unreleased production modifications might exist in the plugin source code at src/main/java/Hello.java" - )); + "Unreleased production modifications might exist in the plugin source code at src/main/java/Hello.java", + probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -501,11 +467,7 @@ void shouldBeAbleToOnlyDisplayProductionFilesInCommit() throws IOException, GitA final String pluginName = "test-plugin"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); @@ -532,10 +494,10 @@ void shouldBeAbleToOnlyDisplayProductionFilesInCommit() throws IOException, GitA assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure( + .isEqualTo(ProbeResult.success( HasUnreleasedProductionChangesProbe.KEY, - "Unreleased production modifications might exist in the plugin source code at src/main/java/Hello.java" - )); + "Unreleased production modifications might exist in the plugin source code at src/main/java/Hello.java", + probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -549,11 +511,7 @@ void shouldBeAbleToDisplayProductionFilesInDifferentCommits() throws IOException final String pluginName = "test-plugin"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); @@ -585,10 +543,10 @@ void shouldBeAbleToDisplayProductionFilesInDifferentCommits() throws IOException assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure( + .isEqualTo(ProbeResult.success( HasUnreleasedProductionChangesProbe.KEY, - "Unreleased production modifications might exist in the plugin source code at src/main/java/AnotherClass.java, src/main/java/Hello.java" - )); + "Unreleased production modifications might exist in the plugin source code at src/main/java/AnotherClass.java, src/main/java/Hello.java", + probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } @@ -602,11 +560,7 @@ void shouldBeAbleToDisplayProductionFilesInDifferentCommitsWithIntermediateCommi final String pluginName = "test-plugin"; when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn(repository); + when(ctx.getScmRepository()).thenReturn(Optional.of(repository)); when(plugin.getName()).thenReturn(pluginName); when(plugin.getScm()).thenReturn(scmLink); @@ -648,10 +602,10 @@ void shouldBeAbleToDisplayProductionFilesInDifferentCommitsWithIntermediateCommi assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure( + .isEqualTo(ProbeResult.success( HasUnreleasedProductionChangesProbe.KEY, - "Unreleased production modifications might exist in the plugin source code at src/main/java/AnotherClass.java, src/main/java/Hello.java" - )); + "Unreleased production modifications might exist in the plugin source code at src/main/java/AnotherClass.java, src/main/java/Hello.java", + probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbeTest.java index f1d06f99e..44cc46358 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IncrementalBuildDetectionProbeTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.when; import java.nio.file.Path; -import java.util.Map; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; @@ -25,10 +25,6 @@ public void init() { plugin = mock(Plugin.class); ctx = mock(ProbeContext.class); probe = getSpy(); - - when(plugin.getDetails()).thenReturn(Map.of( - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "") - )); } @Override @@ -38,89 +34,89 @@ IncrementalBuildDetectionProbe getSpy() { @Test void shouldReturnASuccessfulCheckWhenIncrementalBuildConfiguredInBothFiles() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/plugin-repo-with-correct-configuration")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/plugin-repo-with-correct-configuration"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldReturnFailureWhenIncrementalBuildIsConfiguredOnlyInExtensionsXML() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/plugin-repo-with-missing-maven-config-file")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/plugin-repo-with-missing-maven-config-file"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldReturnFailureWhenIncrementalBuildIsConfiguredOnlyInMavenConfig() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/plugin-repo-with-missing-extensions-file")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/plugin-repo-with-missing-extensions-file"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldFailWhenIncrementalBuildIsIncorrectlyConfiguredInBothFiles() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/plugin-repo-with-incorrect-configuration-lines-in-both-files")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/plugin-repo-with-incorrect-configuration-lines-in-both-files"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldFailWhenIncrementalBuildIsIncorrectlyConfiguredInExtensionsXML() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-plugin-incorrect-extensions-configuration")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/test-plugin-incorrect-extensions-configuration"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldFailWhenIncrementalBuildLinesAreIncorrectInMavenConfig() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-plugin-incorrect-maven-configuration")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/test-plugin-incorrect-maven-configuration"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldFailWhenIncrementalBuildLinesAreMissingInMavenConfig() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-plugin-with-missing-lines-maven-configuration")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/test-plugin-with-missing-lines-maven-configuration"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.")); + .isEqualTo(ProbeResult.success(IncrementalBuildDetectionProbe.KEY, "Incremental Build is not configured in the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } @Test void shouldFailWhenMavenFolderIsNotFound() { - when(ctx.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-repo-without-mvn-should-not-be-found")); + when(ctx.getScmRepository()).thenReturn(Optional.of(Path.of("src/test/resources/jenkinsci/test-repo-without-mvn-should-not-be-found"))); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(IncrementalBuildDetectionProbe.KEY, "Could not find Maven configuration folder for the foo plugin.")); + .isEqualTo(ProbeResult.error(IncrementalBuildDetectionProbe.KEY, "Could not find Maven configuration folder for the foo plugin.", probe.getVersion())); verify(probe).doApply(plugin, ctx); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java index 985d0dd39..6673de416 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java @@ -26,9 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; @@ -36,7 +34,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; import org.assertj.core.api.SoftAssertions; @@ -58,24 +55,24 @@ void doesNotRequireCodeModification() { assertThat(getSpy().isSourceCodeRelated()).isFalse(); } - @SuppressWarnings("unchecked") @Test void shouldRequirePluginToBeInUpdateCenter() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn( + final String pluginName = "foo"; + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of(), - Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.failure(UpdateCenterPluginPublicationProbe.KEY, "") - ) - ); + Map.of(), + List.of() + )); final InstallationStatProbe probe = spy(InstallationStatProbe.class); - for (int i = 0; i < 2; i++) { - assertThat(probe.apply(plugin, ctx).status()).isEqualTo(ResultStatus.ERROR); - verify(probe, never()).doApply(plugin, ctx); - } + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(InstallationStatProbe.KEY, "Could not find plugin " + pluginName + " in Update Center.", probe.getVersion())); } @Test @@ -86,9 +83,6 @@ void shouldBeAbleToFindInstallationCountInUpdateCenter() { final String pluginName = "plugin"; when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, @@ -102,7 +96,7 @@ void shouldBeAbleToFindInstallationCountInUpdateCenter() { SoftAssertions.assertSoftly(softly -> { softly.assertThat(result).isNotNull(); - softly.assertThat(result).extracting("status").isEqualTo(ResultStatus.SUCCESS); + softly.assertThat(result).extracting("status").isEqualTo(ProbeResult.Status.SUCCESS); softly.assertThat(result).extracting("message").isEqualTo("100"); }); } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JSR305ProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JSR305ProbeTest.java index 70f0c096e..ebf15320e 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JSR305ProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JSR305ProbeTest.java @@ -11,7 +11,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Map; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; @@ -29,10 +29,6 @@ public void init() { plugin = mock(Plugin.class); ctx = mock(ProbeContext.class); probe = getSpy(); - - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - )); } @Override @@ -81,13 +77,13 @@ void shouldReturnPluginsThatUseDeprecatedAnnotations() throws IOException { "This-file-should-not-returned." )); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(JSR305Probe.KEY, "Deprecated imports found at foo plugin for test-class-1.java, test-class-2.java, test-class-3.java")); + .isEqualTo(ProbeResult.success(JSR305Probe.KEY, "Deprecated imports found at foo plugin for test-class-1.java, test-class-2.java, test-class-3.java", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } @@ -109,15 +105,13 @@ void shouldNotReturnPluginsWithNoDeprecatedImports() throws IOException { "import java.util.Map;" )); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); when(plugin.getName()).thenReturn("foo"); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.success(JSR305Probe.KEY, "Latest version of imports found at foo plugin.")); + .isEqualTo(ProbeResult.success(JSR305Probe.KEY, "Latest version of imports found at foo plugin.", probe.getVersion())); verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); } - - } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java index 761e0a083..b73dd2677 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java @@ -26,9 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; @@ -36,7 +34,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; import org.junit.jupiter.api.Test; @@ -75,10 +72,8 @@ void shouldFailIfPluginNotInUpdateCenter() { assertThat(result) .isNotNull() - .extracting("id", "status") - .containsExactly(probe.key(), ResultStatus.ERROR); - - verify(probe, never()).doApply(plugin, ctx); + .usingRecursiveComparison().comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(JenkinsCoreProbe.KEY, "Could not find plugin " + pluginName + " in Update Center.", probe.getVersion())); } @Test @@ -88,9 +83,6 @@ void shouldBeAbleToExtractJenkinsVersionFromUpdateCenter() { final ProbeContext ctx = mock(ProbeContext.class); when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, @@ -107,7 +99,9 @@ void shouldBeAbleToExtractJenkinsVersionFromUpdateCenter() { assertThat(result).isNotNull(); assertThat(result) - .extracting("id", "status", "message") - .containsExactly(probe.key(), ResultStatus.SUCCESS, "2.361.1"); + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(JenkinsCoreProbe.KEY, "2.361.1", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbeTest.java index 72f75f8a2..376b64eaa 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsfileProbeTest.java @@ -26,19 +26,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; +import java.nio.file.Path; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import org.junit.jupiter.api.Test; @@ -58,22 +55,21 @@ void shouldBeRelatedToCode() { assertThat(getSpy().isSourceCodeRelated()).isTrue(); } - @SuppressWarnings("unchecked") @Test - void shouldRespectRequirements() { + void shouldRequireLocalRepository() throws IOException { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of(SCMLinkValidationProbe.KEY, ProbeResult.failure("scm", "The plugin SCM link is invalid")) - ); final JenkinsfileProbe probe = getSpy(); - for (int i = 0; i < 2; i++) { - assertThat(probe.apply(plugin, ctx).status()).isEqualTo(ResultStatus.ERROR); - verify(probe, never()).doApply(plugin, ctx); - } + final String pluginName = "foo"; + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getScmRepository()).thenReturn(Optional.empty()); + + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(JenkinsfileProbe.KEY, "There is no local repository for plugin " + pluginName + ".", probe.getVersion())); } @Test @@ -82,15 +78,14 @@ void shouldCorrectlyDetectMissingJenkinsfile() throws IOException { final ProbeContext ctx = mock(ProbeContext.class); final JenkinsfileProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn( - Files.createTempDirectory("foo") - ); + final Path repo = Files.createTempDirectory("foo"); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - assertThat(probe.apply(plugin, ctx).status()).isEqualTo(ResultStatus.FAILURE); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", probe.getVersion())); } @Test @@ -99,16 +94,14 @@ void shouldCorrectlyDetectJenkinsfile() throws IOException { final ProbeContext ctx = mock(ProbeContext.class); final JenkinsfileProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(ctx.getScmRepository()).thenReturn( - Files.createFile( - Paths.get(Files.createTempDirectory("foo").toAbsolutePath().toString(), "Jenkinsfile") - ) - ); - - assertThat(probe.apply(plugin, ctx).status()).isEqualTo(ResultStatus.SUCCESS); + final Path repo = Files.createTempDirectory("foo"); + Files.createFile(repo.resolve("Jenkinsfile")); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); + + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java index 90cf1e042..36fb6ee5d 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java @@ -35,7 +35,6 @@ import java.util.Map; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin; import io.jenkins.pluginhealth.scoring.model.updatecenter.SecurityWarning; import io.jenkins.pluginhealth.scoring.model.updatecenter.SecurityWarningVersion; @@ -70,8 +69,11 @@ void shouldBeOKWithNoSecurityWarning() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "No known security vulnerabilities.", probe.getVersion())); } @Test @@ -94,8 +96,11 @@ void shouldBeOKWithWarningOnDifferentPlugin() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "No known security vulnerabilities.", probe.getVersion())); } @Test @@ -122,8 +127,11 @@ void shouldBeOKWithWarningOnOlderVersion() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "No known security vulnerabilities.", probe.getVersion())); } @Test @@ -149,9 +157,11 @@ void shouldNotBeOKWithWarningOnCurrentVersion() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo(warningId); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, warningId, probe.getVersion())); } @Test @@ -177,9 +187,11 @@ void shouldNotBeOKWithWarningWithoutLastVersion() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo(warningId); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, warningId, probe.getVersion())); } @Test @@ -207,9 +219,11 @@ void shouldNotBeOKWithMultipleWarningsWithoutLastVersion() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("%s, %s".formatted(warningId1, warningId2)); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "%s, %s".formatted(warningId1, warningId2), probe.getVersion())); } @Test @@ -237,9 +251,11 @@ void shouldNotBeOKWithWarningWithoutLastVersionAndOneResolved() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo(warningId1); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, warningId1, probe.getVersion())); } @Test @@ -264,7 +280,10 @@ void shouldBeOKWithWarningWithoutLastVersionButOutOfPattern() { ); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "No known security vulnerabilities.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbeTest.java index 772d773a1..3ca54e7a1 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/LastCommitDateProbeTest.java @@ -26,20 +26,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.nio.file.Files; -import java.util.Map; +import java.nio.file.Path; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Optional; import java.util.UUID; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.PersonIdent; import org.junit.jupiter.api.Test; class LastCommitDateProbeTest extends AbstractProbeTest { @@ -58,82 +63,53 @@ void shouldNotBeRelatedToSourceCode() { assertThat(getSpy().isSourceCodeRelated()).isFalse(); } - @SuppressWarnings("unchecked") @Test void shouldBeExecutedAfterSCMLinkValidation() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final LastCommitDateProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, "") - ) - ); + final String pluginName = "foo"; + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getScmRepository()).thenReturn(Optional.empty()); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(LastCommitDateProbe.KEY, "")); - verify(probe, never()).doApply(plugin, ctx); - - assertThat(probe.apply(plugin, ctx)) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(LastCommitDateProbe.KEY, "")); - verify(probe, never()).doApply(plugin, ctx); + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(LastCommitDateProbe.KEY, "There is no local repository for plugin " + pluginName + ".", probe.getVersion())); } @Test - void shouldReturnSuccessStatusOnValidSCM() throws IOException { + void shouldReturnSuccessStatusOnValidSCM() throws IOException, GitAPIException { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final LastCommitDateProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success("scm", "The plugin SCM link is valid")) - ); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/parameterized-trigger-plugin.git"); - when(ctx.getScmRepository()).thenReturn(Files.createTempDirectory(UUID.randomUUID().toString())); - final ProbeResult r = probe.apply(plugin, ctx); - - assertThat(r.id()).isEqualTo("last-commit-date"); - assertThat(r.status()).isEqualTo(ResultStatus.SUCCESS); - } - - @Test - void shouldReturnSuccessStatusOnValidSCMWithSubFolder() throws IOException { - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); - final LastCommitDateProbe probe = getSpy(); - - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success("scm", "The plugin SCM link is valid")) - ); - when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/aws-java-sdk-plugin/aws-java-sdk-logs"); - when(ctx.getScmRepository()).thenReturn(Files.createTempDirectory(UUID.randomUUID().toString())); - final ProbeResult r = probe.apply(plugin, ctx); - - assertThat(r.id()).isEqualTo("last-commit-date"); - assertThat(r.status()).isEqualTo(ResultStatus.SUCCESS); - } - - @SuppressWarnings("unchecked") - @Test - void shouldRespectRequirements() { - final Plugin plugin = mock(Plugin.class); - final ProbeContext ctx = mock(ProbeContext.class); + final Path repo = Files.createTempDirectory(UUID.randomUUID().toString()); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); + + final ZoneId commitZoneId = ZoneId.of("Europe/Paris"); + final ZonedDateTime commitDate = ZonedDateTime.now(commitZoneId) + .minusHours(1).minusMinutes(2) + .truncatedTo(ChronoUnit.SECONDS); + + try (Git git = Git.init().setDirectory(repo.toFile()).call()) { + git.commit() + .setAllowEmpty(true) + .setSign(false) + .setMessage("This commit") + .setCommitter(new PersonIdent("Foo", "foo@bar.xyz", commitDate.toInstant(), commitZoneId)) + .call(); + } - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of(SCMLinkValidationProbe.KEY, ProbeResult.failure("scm", "The plugin SCM link is invalid")) - ); - final LastCommitDateProbe probe = getSpy(); + final ProbeResult result = probe.apply(plugin, ctx); + assertThat(result) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status") + .isEqualTo(ProbeResult.success(LastCommitDateProbe.KEY, "", probe.getVersion())); - for (int i = 0; i < 2; i++) { - assertThat(probe.apply(plugin, ctx).status()).isEqualTo(ResultStatus.ERROR); - verify(probe, never()).doApply(plugin, ctx); - } + final ZonedDateTime parsedDateTime = ZonedDateTime.parse(result.message(), DateTimeFormatter.ISO_DATE_TIME); + assertThat(parsedDateTime).isEqualTo(commitDate); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbeTest.java index cfb113c30..34094f2cd 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/PullRequestProbeTest.java @@ -27,14 +27,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; @@ -52,11 +50,6 @@ PullRequestProbe getSpy() { return spy(PullRequestProbe.class); } - @Test - void shouldUsePullRequestKey() { - assertThat(getSpy().key()).isEqualTo("pull-request"); - } - @Test void shouldNotRequireNewRelease() { assertThat(getSpy().requiresRelease()).isFalse(); @@ -68,31 +61,17 @@ void shouldNotBeRelatedToSourceCode() { } @Test - void shouldHaveDescription() { - assertThat(getSpy().getDescription()).isNotBlank(); - } - - @SuppressWarnings("unchecked") - @Test - void shouldNotRunWithInvalidSCMLink() { + void shouldSurviveInvalidSCMLink() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn( - Map.of(), - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, "not valid") - ) - ); + when(plugin.getName()).thenReturn("foo-bar"); final PullRequestProbe probe = getSpy(); - for (int i = 0; i < 2; i++) { - assertThat(probe.apply(plugin, ctx)) - .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(PullRequestProbe.KEY, "")); - verify(probe, never()).doApply(plugin, ctx); - } + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(PullRequestProbe.KEY, "Plugin SCM is unknown, cannot fetch the number of open pull requests.", probe.getVersion())); } @Test @@ -103,12 +82,9 @@ void shouldBeAbleToCountOpenPullRequest() throws IOException { final GitHub gh = mock(GitHub.class); final GHRepository ghRepository = mock(GHRepository.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "valid") - )); when(ctx.getGitHub()).thenReturn(gh); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/mailer-plugin"); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of("jenkinsci/mailer-plugin")); + when(ctx.getRepositoryName()).thenReturn(Optional.of("jenkinsci/mailer-plugin")); when(gh.getRepository(anyString())).thenReturn(ghRepository); final List ghPullRequests = List.of( new GHPullRequest(), @@ -126,7 +102,7 @@ void shouldBeAbleToCountOpenPullRequest() throws IOException { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(PullRequestProbe.KEY, "%d".formatted(ghPullRequests.size()))); + .isEqualTo(ProbeResult.success(PullRequestProbe.KEY, "%d".formatted(ghPullRequests.size()), probe.getVersion())); } @Test @@ -136,12 +112,9 @@ void shouldReturnsErrorIfCommunicationWithGitHubIsImpossible() throws IOExceptio final GitHub gh = mock(GitHub.class); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "valid") - )); when(ctx.getGitHub()).thenReturn(gh); when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/mailer-plugin"); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of("jenkinsci/mailer-plugin")); + when(ctx.getRepositoryName()).thenReturn(Optional.of("jenkinsci/mailer-plugin")); when(gh.getRepository(anyString())).thenThrow(IOException.class); final PullRequestProbe probe = getSpy(); @@ -151,8 +124,7 @@ void shouldReturnsErrorIfCommunicationWithGitHubIsImpossible() throws IOExceptio assertThat(result) .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error(PullRequestProbe.KEY, "")); - + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(PullRequestProbe.KEY, "Cannot access repository " + plugin.getScm(), probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbeTest.java index 83fd80de1..08b0fdb21 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/RenovateProbeTest.java @@ -1,26 +1,18 @@ package io.jenkins.pluginhealth.scoring.probes; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; -import java.util.stream.Stream; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; public class RenovateProbeTest extends AbstractProbeTest { @Override @@ -38,57 +30,35 @@ void shouldBeRelatedToCode() { assertThat(getSpy().isSourceCodeRelated()).isTrue(); } - static Stream probeResults() { - return Stream.of( - arguments(// Nothing - Map.of() - ), - arguments( - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, "") - ) - ), - arguments( - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - ) - ), - arguments( - Map.of( - LastCommitDateProbe.KEY, ProbeResult.failure(LastCommitDateProbe.KEY, "") - ) - ), - arguments( - Map.of( - LastCommitDateProbe.KEY, ProbeResult.failure(LastCommitDateProbe.KEY, ""), - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") - ) - ), - arguments( - Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.failure(LastCommitDateProbe.KEY, "") - ) - ) - ); + @Test + void shouldSurvivePluginWithoutLocalRepository() { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + + when(plugin.getName()).thenReturn("foo"); + when(ctx.getScmRepository()).thenReturn(Optional.empty()); + + final RenovateProbe probe = getSpy(); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(RenovateProbe.KEY, "There is no local repository for plugin " + plugin.getName() + ".", probe.getVersion())); } - @ParameterizedTest - @MethodSource("probeResults") - void shouldRequireValidSCMAndLastCommit(Map details) { + @Test + void shouldDetectMissingGitHubActionFolder() throws Exception { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); + final RenovateProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(details); + final Path repo = Files.createTempDirectory("foo"); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final RenovateProbe probe = getSpy(); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.error(RenovateProbe.KEY, "renovate does not meet the criteria to be executed on null")); - - verify(probe, never()).doApply(plugin, ctx); + .isEqualTo(ProbeResult.success(RenovateProbe.KEY, "No GitHub configuration folder found.", probe.getVersion())); } @Test @@ -97,18 +67,14 @@ void shouldDetectMissingRenovateFile() throws Exception { final ProbeContext ctx = mock(ProbeContext.class); final RenovateProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); - when(ctx.getScmRepository()).thenReturn(repo); + Files.createDirectory(repo.resolve(".github")); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.failure(RenovateProbe.KEY, "No GitHub configuration folder found")); - verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); + .isEqualTo(ProbeResult.success(RenovateProbe.KEY, "renovate is not configured.", probe.getVersion())); } @Test @@ -117,21 +83,15 @@ void shouldDetectRenovateFile() throws Exception { final ProbeContext ctx = mock(ProbeContext.class); final RenovateProbe probe = getSpy(); - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); final Path repo = Files.createTempDirectory("foo"); final Path github = Files.createDirectories(repo.resolve(".github")); Files.createFile(github.resolve("renovate.json")); - - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); assertThat(probe.apply(plugin, ctx)) .usingRecursiveComparison() .comparingOnlyFields("id", "message", "status") - .isEqualTo(ProbeResult.success(RenovateProbe.KEY, "renovate is configured")); - verify(probe).doApply(any(Plugin.class), any(ProbeContext.class)); + .isEqualTo(ProbeResult.success(RenovateProbe.KEY, "renovate is configured.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbeTest.java index e1e1b8be7..234d6312a 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SCMLinkValidationProbeTest.java @@ -25,23 +25,18 @@ package io.jenkins.pluginhealth.scoring.probes; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.Map; import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; -import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; import org.junit.jupiter.api.Test; import org.kohsuke.github.GHRepository; @@ -59,209 +54,115 @@ void shouldRequireRelease() { } @Test - void shouldRequirePluginToBeInUpdateCenter() { + void shouldNotAcceptNullNorEmptyScm() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getDetails()).thenReturn(Map.of()); + when(plugin.getScm()).thenReturn( + null, "" + ); final SCMLinkValidationProbe probe = getSpy(); assertThat(probe.apply(plugin, ctx)) + .isNotNull() .usingRecursiveComparison() - .comparingOnlyFields("id", "status") - .isEqualTo(ProbeResult.error("scm", "")); - verify(probe, never()).doApply(plugin, ctx); - } - - @Test - void shouldNotAcceptNullNorEmptyScm() { - final Plugin p1 = mock(Plugin.class); - final ProbeContext ctxP1 = mock(ProbeContext.class); - final Plugin p2 = mock(Plugin.class); - final ProbeContext ctxP2 = mock(ProbeContext.class); - final SCMLinkValidationProbe probe = getSpy(); - - when(p1.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); - when(p1.getScm()).thenReturn(null); - when(p2.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); - when(p2.getScm()).thenReturn(""); - - final ProbeResult r1 = probe.apply(p1, ctxP1); - final ProbeResult r2 = probe.apply(p2, ctxP2); + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(SCMLinkValidationProbe.KEY, "The plugin SCM link is empty.", probe.getVersion())); - assertThat(r1.status()).isEqualTo(ResultStatus.ERROR); - assertThat(r2.status()).isEqualTo(ResultStatus.ERROR); - assertThat(r1.message()).isEqualTo("The plugin SCM link is empty."); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(SCMLinkValidationProbe.KEY, "The plugin SCM link is empty.", probe.getVersion())); } @Test void shouldRecognizeIncorrectSCMUrl() { - final Plugin p1 = mock(Plugin.class); + final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final SCMLinkValidationProbe probe = getSpy(); - when(p1.getScm()).thenReturn("this-is-not-correct"); - when(p1.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); - final ProbeResult r1 = probe.apply(p1, ctx); + when(plugin.getScm()).thenReturn("this-is-not-correct"); - assertThat(r1.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(r1.message()).isEqualTo("SCM link doesn't match GitHub plugin repositories."); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(SCMLinkValidationProbe.KEY, "SCM link doesn't match GitHub plugin repositories.", probe.getVersion())); } @Test void shouldRecognizeCorrectGitHubUrl() throws IOException { - final Plugin p1 = mock(Plugin.class); - ProbeContext contextSpy = spy(new ProbeContext(p1.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); final GitHub gh = mock(GitHub.class); final String repositoryName = "jenkinsci/test-repo"; - when(p1.getScm()).thenReturn("https://github.com/" + repositoryName); - when(p1.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + when(plugin.getName()).thenReturn("test-repo"); + when(plugin.getScm()).thenReturn("https://github.com/" + repositoryName); + when(ctx.getScmRepository()).thenReturn(Optional.of( + Path.of("src/test/resources/jenkinsci/test-repo/test-nested-dir-1/test-nested-dir-2") )); - - when(contextSpy.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-repo/test-nested-dir-1")); - when(contextSpy.getGitHub()).thenReturn(gh); - GHRepository repository = mock(GHRepository.class); - when(gh.getRepository(repositoryName)).thenReturn(repository); - - when(p1.getName()).thenReturn("test-repo"); - when(contextSpy.getRepositoryName(anyString())).thenReturn(Optional.of(repositoryName)); + when(ctx.getGitHub()).thenReturn(gh); + when(gh.getRepository(repositoryName)).thenReturn(new GHRepository()); final SCMLinkValidationProbe probe = getSpy(); - final ProbeResult r1 = probe.apply(p1, contextSpy); - assertThat(contextSpy.getScmFolderPath()).isEqualTo(Optional.of("test-nested-dir-2")); - assertThat(r1.status()).isEqualTo(ResultStatus.SUCCESS); - assertThat(r1.message()).isEqualTo("The plugin SCM link is valid."); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(SCMLinkValidationProbe.KEY, "The plugin SCM link is valid.", probe.getVersion())); } @Test void shouldRecognizeInvalidGitHubUrl() throws Exception { - final Plugin p1 = mock(Plugin.class); + final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final GitHub gh = mock(GitHub.class); final String repositoryName = "jenkinsci/this-is-not-going-to-work"; - when(p1.getScm()).thenReturn("https://github.com/" + repositoryName); - when(p1.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); + when(plugin.getScm()).thenReturn("https://github.com/" + repositoryName); + when(plugin.getName()).thenReturn("foo"); + + when(ctx.getScmRepository()).thenReturn(Optional.of(Files.createTempDirectory("foo"))); when(ctx.getGitHub()).thenReturn(gh); when(gh.getRepository(repositoryName)).thenThrow(IOException.class); final SCMLinkValidationProbe probe = getSpy(); - final ProbeResult r1 = probe.apply(p1, ctx); - assertThat(r1.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(r1.message()).isEqualTo("The plugin SCM link is invalid."); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success("scm", "The plugin SCM link is invalid.", probe.getVersion())); } @Test - void shouldReturnCorrectScmFolderPath() throws IOException { + void shouldBeAbleToFindPluginInModule() throws IOException { final Plugin plugin = mock(Plugin.class); - final GitHub github = mock(GitHub.class); - final String repositoryName = "jenkinsci/test-repo"; - ProbeContext contextSpy = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); - - when(plugin.getScm()).thenReturn("https://github.com/" + repositoryName); - when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); - when(plugin.getName()).thenReturn("test-repo"); - - when(contextSpy.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-repo/test-nested-dir-1")); - when(contextSpy.getGitHub()).thenReturn(github); - GHRepository repository = mock(GHRepository.class); - when(github.getRepository(repositoryName)).thenReturn(repository); - - final SCMLinkValidationProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, contextSpy); - - assertThat(contextSpy.getScmFolderPath()).isEqualTo(Optional.of("test-nested-dir-2")); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); - assertThat(result.message()).isEqualTo("The plugin SCM link is valid."); - verify(probe).doApply(plugin, contextSpy); - } + final ProbeContext ctx = mock(ProbeContext.class); - @Test - void shouldNotReturnInCorrectScmFolderPath() throws IOException { - final Plugin plugin = mock(Plugin.class); - final GitHub github = mock(GitHub.class); - final String repositoryName = "jenkinsci/test-repo"; - ProbeContext contextSpy = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final GitHub gh = mock(GitHub.class); + final GHRepository ghRepo = mock(GHRepository.class); - when(plugin.getScm()).thenReturn("https://github.com/" + repositoryName); - when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); + when(plugin.getScm()).thenReturn("https://github.com/jenkinsci/this-is-fine"); when(plugin.getName()).thenReturn("test-repo"); - when(contextSpy.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-repo/test-incorrect-nested-dir-1")); - when(contextSpy.getGitHub()).thenReturn(github); - - final SCMLinkValidationProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, contextSpy); - - assertThat(contextSpy.getScmFolderPath()).isEqualTo(null); - assertThat(result.status()).isEqualTo(ResultStatus.ERROR); - assertThat(result.message()).isEqualTo("No valid POM file found in test-repo plugin."); - verify(probe).doApply(plugin, contextSpy); - } - - @Test - void shouldFindRootScmFolderPath() throws IOException { - final Plugin plugin = mock(Plugin.class); - final GitHub github = mock(GitHub.class); - final String repositoryName = "jenkinsci/test-repo"; - ProbeContext ctx = new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of())); - ProbeContext contextSpy = spy(ctx); - - when(plugin.getScm()).thenReturn("https://github.com/" + repositoryName); - when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + when(ctx.getScmRepository()).thenReturn(Optional.of( + Path.of("src/test/resources/jenkinsci/test-repo/test-nested-dir-1") )); - when(plugin.getName()).thenReturn("test-repo"); - - when(contextSpy.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-repo/test-direct-dir")); - when(contextSpy.getGitHub()).thenReturn(github); + when(ctx.getGitHub()).thenReturn(gh); + when(gh.getRepository("jenkinsci/this-is-fine")).thenReturn(ghRepo); final SCMLinkValidationProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, contextSpy); - - assertThat(contextSpy.getScmFolderPath()).isEqualTo(Optional.of("test-direct-dir")); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); - assertThat(result.message()).isEqualTo("The plugin SCM link is valid."); - verify(probe).doApply(plugin, contextSpy); - } - - @Test - void shouldFailWhenPomFileDoesNotExistsInTheRepository() throws IOException { - final Plugin plugin = mock(Plugin.class); - final GitHub github = mock(GitHub.class); - final String repositoryName = "jenkinsci/test-no-pom-file-repo"; - ProbeContext contextSpy = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final ProbeResult result = probe.apply(plugin, ctx); - when(plugin.getScm()).thenReturn("https://github.com/" + repositoryName); - when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") - )); - when(plugin.getName()).thenReturn("test-repo"); - - when(contextSpy.getScmRepository()).thenReturn(Path.of("src/test/resources/jenkinsci/test-no-pom-file-repo")); - when(contextSpy.getGitHub()).thenReturn(github); - - final SCMLinkValidationProbe probe = getSpy(); - final ProbeResult result = probe.apply(plugin, contextSpy); + assertThat(result) + .usingRecursiveComparison() + .comparingOnlyFields("id", "message", "status") + .isEqualTo(ProbeResult.success(probe.key(), "The plugin SCM link is valid.", probe.getVersion())); - assertThat(result.status()).isEqualTo(ResultStatus.ERROR); - assertThat(result.message()).isEqualTo("No valid POM file found in test-repo plugin."); - verify(probe).doApply(plugin, contextSpy); + verify(ctx).setScmFolderPath(Path.of("test-nested-dir-2")); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbeTest.java index 19bb20c9e..4be49d0b7 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SecurityScanProbeTest.java @@ -33,11 +33,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Map; +import java.util.Optional; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,11 +51,6 @@ public void init() { plugin = mock(Plugin.class); ctx = mock(ProbeContext.class); probe = getSpy(); - - when(plugin.getDetails()).thenReturn(Map.of( - SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); } @Override @@ -66,22 +60,27 @@ SecurityScanProbe getSpy() { @Test void shouldBeAbleToDetectRepositoryWithNoGitHubWorkflowConfigured() throws IOException { - when(ctx.getScmRepository()).thenReturn(Files.createTempDirectory("foo")); + final Path repo = Files.createTempDirectory("foo"); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("Plugin has no GitHub Action configured"); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(SecurityScanProbe.KEY, "Plugin has no GitHub Action configured.", probe.getVersion())); } @Test void shouldBeAbleToDetectRepositoryWithNoSecurityScanConfigured() throws IOException { final Path repo = Files.createTempDirectory("foo"); Files.createDirectories(repo.resolve(".github/workflows")); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("GitHub workflow security scan is not configured in the plugin"); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(SecurityScanProbe.KEY, "GitHub workflow security scan is not configured in the plugin.", probe.getVersion())); } @Test @@ -96,11 +95,13 @@ void shouldNotFindSecurityScanConfiguredInGitHubWorkflow() throws IOException { " security-scan-name:", " uses: this-is-not-the-workflow-we-are-looking-for" )); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); - assertThat(result.message()).isEqualTo("GitHub workflow security scan is not configured in the plugin"); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(SecurityScanProbe.KEY, "GitHub workflow security scan is not configured in the plugin.", probe.getVersion())); } @Test @@ -115,11 +116,13 @@ void shouldSucceedIfSecurityScanIsConfigured() throws IOException { " this-is-a-valid-security-scan:", " uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml" )); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); - assertThat(result.message()).isEqualTo("GitHub workflow security scan is configured in the plugin"); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(SecurityScanProbe.KEY, "GitHub workflow security scan is configured in the plugin.", probe.getVersion())); } @Test @@ -134,10 +137,12 @@ void shouldSucceedToFindWorkflowEvenWithVersion() throws IOException { " security-scan:", " uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v42" )); - when(ctx.getScmRepository()).thenReturn(repo); + when(ctx.getScmRepository()).thenReturn(Optional.of(repo)); - final ProbeResult result = probe.apply(plugin, ctx); - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); - assertThat(result.message()).isEqualTo("GitHub workflow security scan is configured in the plugin"); + assertThat(probe.apply(plugin, ctx)) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(SecurityScanProbe.KEY, "GitHub workflow security scan is configured in the plugin.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java index 241bf51f3..2a9e42889 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java @@ -74,14 +74,7 @@ public void shouldFailWhenRepositoryIsNotInOrganization() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getName()).thenReturn(pluginName); - when(plugin.getScm()).thenReturn(scmLink); - when(plugin.getDetails()).thenReturn(Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( @@ -93,15 +86,16 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 Map.of(), List.of() )); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.empty()); + when(ctx.getRepositoryName()).thenReturn(Optional.empty()); final SpotBugsProbe probe = getSpy(); final ProbeResult result = probe.apply(plugin, ctx); assertThat(result) + .isNotNull() .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(SpotBugsProbe.KEY, "Cannot determine plugin repository")); + .isEqualTo(ProbeResult.error(SpotBugsProbe.KEY, "Cannot determine plugin repository.", probe.getVersion())); } @SuppressWarnings("unchecked") @@ -119,12 +113,6 @@ public void shouldBeAbleToRetrieveDetailsFromGitHubChecks() throws IOException { final GHRepository ghRepository = mock(GHRepository.class); when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( @@ -136,7 +124,7 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 List.of() )); when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); + when(ctx.getRepositoryName()).thenReturn(Optional.of(pluginRepo)); when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); @@ -153,7 +141,7 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(SpotBugsProbe.KEY, "SpotBugs found in build configuration")); + .isEqualTo(ProbeResult.success(SpotBugsProbe.KEY, "SpotBugs found in build configuration.", probe.getVersion())); } @SuppressWarnings("unchecked") @@ -171,12 +159,6 @@ public void shouldFailIfThereIsNoSpotBugs() throws IOException { final GHRepository ghRepository = mock(GHRepository.class); when(plugin.getName()).thenReturn(pluginName); - when(plugin.getDetails()).thenReturn(Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, ""), - LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, "") - )); - when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( @@ -188,7 +170,7 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 List.of() )); when(ctx.getGitHub()).thenReturn(gh); - when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(pluginRepo)); + when(ctx.getRepositoryName()).thenReturn(Optional.of(pluginRepo)); when(gh.getRepository(pluginRepo)).thenReturn(ghRepository); final PagedIterable checkRuns = (PagedIterable) mock(PagedIterable.class); @@ -201,6 +183,6 @@ pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0 assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(SpotBugsProbe.KEY, "SpotBugs not found in build configuration")); + .isEqualTo(ProbeResult.success(SpotBugsProbe.KEY, "SpotBugs not found in build configuration.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java index dc673db6d..82a5d08c4 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java @@ -35,7 +35,6 @@ import java.util.Map; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; @@ -67,8 +66,11 @@ void shouldBeAbleToDetectPluginForAdoption() { )); final ProbeResult result = upForAdoptionProbe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is up for adoption.", upForAdoptionProbe.getVersion())); } @Test @@ -85,8 +87,11 @@ void shouldBeAbleToDetectPluginNotForAdoption() { )); final ProbeResult result = upForAdoptionProbe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.SUCCESS); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", upForAdoptionProbe.getVersion())); } @Test @@ -95,7 +100,6 @@ void shouldFailWhenPluginNotPresentInUpdateCenter() { final ProbeContext ctx = mock(ProbeContext.class); when(plugin.getName()).thenReturn("foo"); - when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of(), Map.of(), @@ -104,7 +108,10 @@ void shouldFailWhenPluginNotPresentInUpdateCenter() { final UpForAdoptionProbe probe = getSpy(); final ProbeResult result = probe.apply(plugin, ctx); - - assertThat(result.status()).isEqualTo(ResultStatus.FAILURE); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(UpForAdoptionProbe.KEY, "This plugin is not in the update-center.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java index 59ff0bd49..f5150fc67 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java @@ -51,7 +51,7 @@ void shouldNotRequireRelease() { } @Test - void shouldFailIfPluginIsNotInUpdateCenterMap() { + void shouldFailIfPluginIsNotInUpdateCenter() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final String pluginName = "foo"; @@ -69,11 +69,11 @@ void shouldFailIfPluginIsNotInUpdateCenterMap() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.failure(UpdateCenterPluginPublicationProbe.KEY, "This plugin's publication has been stopped by the update-center")); + .isEqualTo(ProbeResult.error(UpdateCenterPluginPublicationProbe.KEY, "This plugin's publication has been stopped by the update-center.", probe.getVersion())); } @Test - void shouldSucceedIfPluginIsInUpdateCenterMap() { + void shouldSucceedIfPluginIsInUpdateCenter() { final Plugin plugin = mock(Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final String pluginName = "foo"; @@ -93,6 +93,6 @@ void shouldSucceedIfPluginIsInUpdateCenterMap() { assertThat(result) .usingRecursiveComparison() .comparingOnlyFields("id", "status", "message") - .isEqualTo(ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "This plugin is still actively published by the update-center")); + .isEqualTo(ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "This plugin is still actively published by the update-center.", probe.getVersion())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AbstractScoringTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AbstractScoringTest.java index 894f42246..d86e4bf8e 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AbstractScoringTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AbstractScoringTest.java @@ -25,15 +25,7 @@ package io.jenkins.pluginhealth.scoring.scores; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import java.util.Map; - -import io.jenkins.pluginhealth.scoring.model.Plugin; -import io.jenkins.pluginhealth.scoring.model.ScoreResult; - -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; public abstract class AbstractScoringTest { @@ -48,25 +40,4 @@ void shouldHaveValidName() { void shouldHaveDescription() { assertThat(getSpy().description()).isNotBlank(); } - - @Test - void shouldNotHaveNullNotEmptyRequirements() { - assertThat(getSpy().getScoreComponents()).isNotEmpty(); - } - - @Test - void shouldReturnNonNullScoreResult() { - final Plugin plugin = mock(Plugin.class); - final T scoring = getSpy(); - - when(scoring.getScoreComponents()).thenReturn(Map.of("foo", 1f)); - final ScoreResult score = scoring.apply(plugin); - - assertThat(score.key()).isEqualTo(scoring.key()); - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(score.key()).isEqualTo(scoring.key()); - softly.assertThat(score.coefficient()).isEqualTo(scoring.coefficient()); - softly.assertThat(score.value()).isEqualTo(0f); - }); - } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoringTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoringTest.java index 023567521..6824a320d 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoringTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/AdoptionScoringTest.java @@ -30,11 +30,11 @@ import static org.mockito.Mockito.when; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Map; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.ScoreResult; import io.jenkins.pluginhealth.scoring.probes.LastCommitDateProbe; import io.jenkins.pluginhealth.scoring.probes.UpForAdoptionProbe; @@ -52,13 +52,30 @@ void shouldScoreZeroForPluginsUpForAdoption() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); - when(plugin.getDetails()).thenReturn( - Map.of(UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.FAILURE)) - ); + when(plugin.getDetails()).thenReturn(Map.of( + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is up for adoption.", 1) + )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("adoption"); - assertThat(result.coefficient()).isEqualTo(.8f); + assertThat(result.weight()).isEqualTo(.8f); + assertThat(result.value()).isEqualTo(0); + } + + @Test + void shouldScoreZeroForPluginsUpForAdoptionEvenWithRecentCommit() { + final AdoptionScoring scoring = getSpy(); + final Plugin plugin = mock(Plugin.class); + + when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusHours(4)); + when(plugin.getDetails()).thenReturn(Map.of( + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is up for adoption.", 1), + LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).format(DateTimeFormatter.ISO_DATE_TIME), 1) + )); + + final ScoreResult result = scoring.apply(plugin); + assertThat(result.key()).isEqualTo("adoption"); + assertThat(result.weight()).isEqualTo(.8f); assertThat(result.value()).isEqualTo(0); } @@ -68,7 +85,7 @@ void shouldScoreZeroForPluginsWithNoLastCommit() { final Plugin plugin = mock(Plugin.class); when(plugin.getDetails()).thenReturn( - Map.of(UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.SUCCESS)) + Map.of(UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1)) ); final ScoreResult result = scoring.apply(plugin); @@ -76,71 +93,71 @@ void shouldScoreZeroForPluginsWithNoLastCommit() { } @Test - void shouldScoreOneForPluginsWithCommitsLessThanSixMonthsOld() { + void shouldScoreHundredForPluginsWithCommitsLessThanSixMonthsOld() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusMonths(2)); when(plugin.getDetails()).thenReturn( Map.of( - UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.SUCCESS), - LastCommitDateProbe.KEY, new ProbeResult(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).toString(), ResultStatus.SUCCESS) + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1), + LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).format(DateTimeFormatter.ISO_DATE_TIME), 1) ) ); final ScoreResult result = scoring.apply(plugin); - assertThat(result.value()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(100); } @Test - void shouldScoreSeventyFiveForPluginsWithCommitsLessThanOneYearOld() { + void shouldScoreEightyForPluginsWithCommitsLessThanOneYearOld() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusMonths(8)); when(plugin.getDetails()).thenReturn( Map.of( - UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.SUCCESS), - LastCommitDateProbe.KEY, new ProbeResult(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).toString(), ResultStatus.SUCCESS) + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1), + LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).format(DateTimeFormatter.ISO_DATE_TIME), 1) ) ); final ScoreResult result = scoring.apply(plugin); - assertThat(result.value()).isEqualTo(.75f); + assertThat(result.value()).isEqualTo(80); } @Test - void shouldScoreFiftyForPluginsWithCommitsLessThanTwoYearsOld() { + void shouldScoreSixtyForPluginsWithCommitsLessThanTwoYearsOld() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusMonths(18)); when(plugin.getDetails()).thenReturn( Map.of( - UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.SUCCESS), - LastCommitDateProbe.KEY, new ProbeResult(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).toString(), ResultStatus.SUCCESS) + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1), + LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).format(DateTimeFormatter.ISO_DATE_TIME), 1) ) ); final ScoreResult result = scoring.apply(plugin); - assertThat(result.value()).isEqualTo(.5f); + assertThat(result.value()).isEqualTo(60); } @Test - void shouldScoreTwentyFiveForPluginsWithCommitsLessThanFourYearsOld() { + void shouldScoreFortyForPluginsWithCommitsLessThanFourYearsOld() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusYears(3)); when(plugin.getDetails()).thenReturn( Map.of( - UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.SUCCESS), - LastCommitDateProbe.KEY, new ProbeResult(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).toString(), ResultStatus.SUCCESS) + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1), + LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).format(DateTimeFormatter.ISO_DATE_TIME), 1) ) ); final ScoreResult result = scoring.apply(plugin); - assertThat(result.value()).isEqualTo(.25f); + assertThat(result.value()).isEqualTo(40); } @Test @@ -148,15 +165,15 @@ void shouldScoreZeroForPluginsWithCommitsMoreThanFourYearsOld() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); - when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusYears(4)); + when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusYears(5)); when(plugin.getDetails()).thenReturn( Map.of( - UpForAdoptionProbe.KEY, new ProbeResult(UpForAdoptionProbe.KEY, "", ResultStatus.SUCCESS), - LastCommitDateProbe.KEY, new ProbeResult(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).toString(), ResultStatus.SUCCESS) + UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1), + LastCommitDateProbe.KEY, ProbeResult.success(LastCommitDateProbe.KEY, ZonedDateTime.now().minusHours(3).format(DateTimeFormatter.ISO_DATE_TIME), 1) ) ); final ScoreResult result = scoring.apply(plugin); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.value()).isEqualTo(0); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoringTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoringTest.java index 6e8732de1..a061638cb 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoringTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/DeprecatedPluginScoringTest.java @@ -33,7 +33,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.ScoreResult; import io.jenkins.pluginhealth.scoring.probes.DeprecatedPluginProbe; @@ -51,14 +50,14 @@ void shouldScoreCorrectlyNotDeprecatedPlugin() { final DeprecatedPluginScoring scoring = getSpy(); when(plugin.getDetails()).thenReturn(Map.of( - DeprecatedPluginProbe.KEY, new ProbeResult(DeprecatedPluginProbe.KEY, "", ResultStatus.SUCCESS) + DeprecatedPluginProbe.KEY, ProbeResult.success(DeprecatedPluginProbe.KEY, "This plugin is NOT deprecated.", 1) )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("deprecation"); - assertThat(result.coefficient()).isEqualTo(.8f); - assertThat(result.value()).isEqualTo(1f); + assertThat(result.weight()).isEqualTo(.8f); + assertThat(result.value()).isEqualTo(100); } @Test @@ -71,8 +70,8 @@ void shouldBadlyScorePluginWithNoProbe() { final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("deprecation"); - assertThat(result.coefficient()).isEqualTo(.8f); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.weight()).isEqualTo(.8f); + assertThat(result.value()).isEqualTo(0); } @Test @@ -81,13 +80,13 @@ void shouldScoreCorrectlyDeprecatedPlugin() { final DeprecatedPluginScoring scoring = getSpy(); when(plugin.getDetails()).thenReturn(Map.of( - DeprecatedPluginProbe.KEY, new ProbeResult(DeprecatedPluginProbe.KEY, "", ResultStatus.FAILURE) + DeprecatedPluginProbe.KEY, ProbeResult.success(DeprecatedPluginProbe.KEY, "This plugin is marked as deprecated.", 1) )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("deprecation"); - assertThat(result.coefficient()).isEqualTo(.8f); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.weight()).isEqualTo(.8f); + assertThat(result.value()).isEqualTo(0); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoringTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoringTest.java index b52607e9e..9bbba4c5f 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoringTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/PluginMaintenanceScoringTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.when; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import io.jenkins.pluginhealth.scoring.model.Plugin; @@ -59,188 +60,188 @@ static Stream probeResultsAndValue() { return Stream.of( arguments(// Nothing Map.of(), - 0f + 0 ), arguments(// All bad Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.failure(DependabotPullRequestProbe.KEY, "1"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "1", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - 0f + 0 ), arguments(// All bad with open dependabot pull request Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.failure(DependabotPullRequestProbe.KEY, "1"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "1", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - 0f + 0 ), arguments(// All good Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - 1f + 100 ), arguments(// Only Jenkinsfile Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .65f + 65 ), arguments(// Jenkinsfile and dependabot but with open pull request Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.failure(DependabotPullRequestProbe.KEY, "1"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "1", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .65f + 65 ), arguments(// Jenkinsfile and dependabot with no open pull request Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .8f + 80 ), arguments(// Jenkinsfile and CD Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .7f + 70 ), arguments(// Jenkinsfile and documentation Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - .8f + 80 ), arguments(// Jenkinsfile and CD and dependabot but with open pull request Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.failure(DependabotPullRequestProbe.KEY, "1"), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "1", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .7f + 70 ), arguments(// Jenkinsfile and CD and dependabot with no open pull request Map.of( - JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .85f + 85 ), arguments(// Dependabot only with no open pull requests Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .15f + 15 ), arguments(// CD only Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .05f + 5 ), arguments(// Dependabot with no open pull request and CD Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.failure(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is not located in the plugin repository.", 1) ), - .2f + 20 ), arguments(// Dependabot with no open pull request and documentation Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - .3f + 30 ), arguments(// Documentation migration only Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - .15f + 15 ), arguments(// Documentation migration and Dependabot but with open pull requests Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.failure(DependabotPullRequestProbe.KEY, "1"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "1", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - .15f + 15 ), arguments(// Documentation migration and Dependabot with no open pull requests Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, ""), - DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0"), - ContinuousDeliveryProbe.KEY, ProbeResult.failure(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is configured.", 1), + DependabotPullRequestProbe.KEY, ProbeResult.success(DependabotPullRequestProbe.KEY, "0", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "Could not find JEP-229 workflow definition.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - .3f + 30 ), arguments(// Documentation migration and CD Map.of( - JenkinsfileProbe.KEY, ProbeResult.failure(JenkinsfileProbe.KEY, ""), - DependabotProbe.KEY, ProbeResult.failure(DependabotProbe.KEY, ""), - ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, ""), - DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "") + JenkinsfileProbe.KEY, ProbeResult.success(JenkinsfileProbe.KEY, "No Jenkinsfile found", 1), + DependabotProbe.KEY, ProbeResult.success(DependabotProbe.KEY, "dependabot is not configured.", 1), + ContinuousDeliveryProbe.KEY, ProbeResult.success(ContinuousDeliveryProbe.KEY, "JEP-229 workflow definition found.", 1), + DocumentationMigrationProbe.KEY, ProbeResult.success(DocumentationMigrationProbe.KEY, "Documentation is located in the plugin repository.", 1) ), - .2f + 20 ) ); } @ParameterizedTest @MethodSource("probeResultsAndValue") - public void shouldScorePluginBasedOnProbeResultMatrix(Map details, float value) { + public void shouldScorePluginBasedOnProbeResultMatrix(Map details, int value) { final Plugin plugin = mock(Plugin.class); final PluginMaintenanceScoring scoring = getSpy(); @@ -248,13 +249,10 @@ public void shouldScorePluginBasedOnProbeResultMatrix(Map d final ScoreResult result = scoring.apply(plugin); - assertThat(result.key()) - .withFailMessage(() -> "Score key should be '%s'".formatted(KEY)) - .isEqualTo(KEY); - assertThat(result.coefficient()) - .withFailMessage(() -> "Score coefficient should be '%f'".formatted(COEFFICIENT)) - .isEqualTo(COEFFICIENT); - assertThat(result.value()) - .isEqualTo(value); + assertThat(result) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("key", "value", "weight") + .isEqualTo(new ScoreResult(KEY, value, COEFFICIENT, Set.of())); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoringTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoringTest.java index 36642b063..12b7a1efa 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoringTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/SecurityWarningScoringTest.java @@ -33,7 +33,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.ScoreResult; import io.jenkins.pluginhealth.scoring.probes.KnownSecurityVulnerabilityProbe; @@ -51,14 +50,14 @@ void shouldBeAbleToDetectPluginWithSecurityWarning() { final SecurityWarningScoring scoring = getSpy(); when(plugin.getDetails()).thenReturn(Map.of( - KnownSecurityVulnerabilityProbe.KEY, new ProbeResult(KnownSecurityVulnerabilityProbe.KEY, "", ResultStatus.FAILURE) + KnownSecurityVulnerabilityProbe.KEY, ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "SECURITY-123, link-to-security-advisory", 1) )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("security"); - assertThat(result.coefficient()).isEqualTo(1f); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.weight()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(0); } @Test @@ -71,8 +70,8 @@ void shouldBadlyScorePluginWithNoProbeResult() { final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("security"); - assertThat(result.coefficient()).isEqualTo(1f); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.weight()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(0); } @Test @@ -81,13 +80,13 @@ void shouldBeAbleToDetectPluginWithNoSecurityWarning() { final SecurityWarningScoring scoring = getSpy(); when(plugin.getDetails()).thenReturn(Map.of( - KnownSecurityVulnerabilityProbe.KEY, new ProbeResult(KnownSecurityVulnerabilityProbe.KEY, "", ResultStatus.SUCCESS) + KnownSecurityVulnerabilityProbe.KEY, ProbeResult.success(KnownSecurityVulnerabilityProbe.KEY, "No known security vulnerabilities.", 1) )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("security"); - assertThat(result.coefficient()).isEqualTo(1f); - assertThat(result.value()).isEqualTo(1f); + assertThat(result.weight()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(100); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoringTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoringTest.java index 7aba38401..dd72240f8 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoringTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/scores/UpdateCenterPublishedPluginDetectionScoringTest.java @@ -33,7 +33,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.ScoreResult; import io.jenkins.pluginhealth.scoring.probes.UpdateCenterPluginPublicationProbe; @@ -51,14 +50,14 @@ public void shouldScoreCorrectlyWhenPluginInUpdateCenter() { final UpdateCenterPublishedPluginDetectionScoring scoring = spy(UpdateCenterPublishedPluginDetectionScoring.class); when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, new ProbeResult(UpdateCenterPluginPublicationProbe.KEY, "", ResultStatus.SUCCESS) + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "This plugin is still actively published by the update-center.", 1) )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("update-center-plugin-publication"); - assertThat(result.coefficient()).isEqualTo(1f); - assertThat(result.value()).isEqualTo(1f); + assertThat(result.weight()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(100); } @Test @@ -71,8 +70,8 @@ public void shouldBadlyScorePluginWithNoProbe() { final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("update-center-plugin-publication"); - assertThat(result.coefficient()).isEqualTo(1f); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.weight()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(0); } @Test @@ -81,13 +80,13 @@ public void shouldScoreBadlyWhenPluginNotInUpdateCenter() { final UpdateCenterPublishedPluginDetectionScoring scoring = getSpy(); when(plugin.getDetails()).thenReturn(Map.of( - UpdateCenterPluginPublicationProbe.KEY, new ProbeResult(UpdateCenterPluginPublicationProbe.KEY, "", ResultStatus.FAILURE) + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "", 1) )); final ScoreResult result = scoring.apply(plugin); assertThat(result.key()).isEqualTo("update-center-plugin-publication"); - assertThat(result.coefficient()).isEqualTo(1f); - assertThat(result.value()).isEqualTo(0f); + assertThat(result.weight()).isEqualTo(1f); + assertThat(result.value()).isEqualTo(0); } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java index 971f3a77b..b33c688c2 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java @@ -29,9 +29,9 @@ import static org.mockito.Mockito.when; import java.time.ZonedDateTime; -import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import io.jenkins.pluginhealth.scoring.AbstractDBContainerTest; import io.jenkins.pluginhealth.scoring.model.Plugin; @@ -69,7 +69,7 @@ void shouldBeAbleToSaveScoreForPlugin() { ); final Score score = new Score(p1, ZonedDateTime.now()); - final ScoreResult result = new ScoreResult("foo", 1, 1); + final ScoreResult result = new ScoreResult("foo", 100, 1, Set.of()); score.addDetail(result); final Score saved = scoreService.save(score); @@ -91,13 +91,13 @@ void shouldBeAbleToExtractScoreSummary() { ); final Score p1s = new Score(p1, ZonedDateTime.now()); - p1s.addDetail(new ScoreResult("foo", 1, 1)); - p1s.addDetail(new ScoreResult("bar", 0, .5f)); + p1s.addDetail(new ScoreResult("foo", 100, 1, Set.of())); + p1s.addDetail(new ScoreResult("bar", 0, .5f, Set.of())); final Score p2s = new Score(p2, ZonedDateTime.now()); - p2s.addDetail(new ScoreResult("foo", 0, 1)); + p2s.addDetail(new ScoreResult("foo", 0, 1, Set.of())); - List.of(p1s, p2s).forEach(scoreService::save); + Set.of(p1s, p2s).forEach(scoreService::save); assertThat(scoreRepository.count()).isEqualTo(2); final Map summary = scoreService.getLatestScoresSummaryMap(); @@ -123,24 +123,24 @@ void shouldOnlyRetrieveLatestScoreForPlugins() { ); final Score p1s = new Score(p1, ZonedDateTime.now()); - p1s.addDetail(new ScoreResult("foo", 1, 1)); - p1s.addDetail(new ScoreResult("bar", 0, .5f)); + p1s.addDetail(new ScoreResult("foo", 1, 1, Set.of())); + p1s.addDetail(new ScoreResult("bar", 0, .5f, Set.of())); final Score p2s = new Score(p2, ZonedDateTime.now()); - p2s.addDetail(new ScoreResult("foo", 0, 1)); + p2s.addDetail(new ScoreResult("foo", 0, 1, Set.of())); final Score p1sOld = new Score(p1, ZonedDateTime.now().minusMinutes(10)); - p1sOld.addDetail(new ScoreResult("foo", 1, 1)); - p1sOld.addDetail(new ScoreResult("bar", 0, .5f)); + p1sOld.addDetail(new ScoreResult("foo", 1, 1, Set.of())); + p1sOld.addDetail(new ScoreResult("bar", 0, .5f, Set.of())); final Score p1sOld2 = new Score(p1, ZonedDateTime.now().minusMinutes(15)); - p1sOld2.addDetail(new ScoreResult("foo", 1, 1)); - p1sOld2.addDetail(new ScoreResult("bar", 0, .5f)); + p1sOld2.addDetail(new ScoreResult("foo", 1, 1, Set.of())); + p1sOld2.addDetail(new ScoreResult("bar", 0, .5f, Set.of())); final Score p2sOld = new Score(p2, ZonedDateTime.now().minusMinutes(10)); - p2sOld.addDetail(new ScoreResult("foo", 0, 1)); + p2sOld.addDetail(new ScoreResult("foo", 0, 1, Set.of())); - List.of(p1s, p2s, p1sOld, p2sOld, p1sOld2).forEach(scoreService::save); + Set.of(p1s, p2s, p1sOld, p2sOld, p1sOld2).forEach(scoreService::save); assertThat(scoreRepository.count()).isEqualTo(5); final Map summary = scoreService.getLatestScoresSummaryMap(); @@ -183,33 +183,33 @@ void shouldBeAbeToRetrieveScoreStatisticsAndIgnoreOldScores() { )); final Score p1s = new Score(p1, ZonedDateTime.now()); - p1s.addDetail(new ScoreResult(s1Key, .5f, .5f)); + p1s.addDetail(new ScoreResult(s1Key, 50, .5f, Set.of())); final Score p1sOld = new Score(p1, ZonedDateTime.now().minusMinutes(10)); - p1sOld.addDetail(new ScoreResult(s1Key, 1, 1)); + p1sOld.addDetail(new ScoreResult(s1Key, 100, 1, Set.of())); final Score p2s = new Score(p2, ZonedDateTime.now()); - p2s.addDetail(new ScoreResult(s1Key, 0, 1)); + p2s.addDetail(new ScoreResult(s1Key, 0, 1, Set.of())); final Score p2sOld = new Score(p2, ZonedDateTime.now().minusMinutes(5)); - p2sOld.addDetail(new ScoreResult(s1Key, .9f, 1)); + p2sOld.addDetail(new ScoreResult(s1Key, 90, 1, Set.of())); final Score p3s = new Score(p3, ZonedDateTime.now()); - p3s.addDetail(new ScoreResult(s1Key, 1, 1)); + p3s.addDetail(new ScoreResult(s1Key, 100, 1, Set.of())); final Score p4s = new Score(p4, ZonedDateTime.now()); - p4s.addDetail(new ScoreResult(s1Key, .75f, 1)); + p4s.addDetail(new ScoreResult(s1Key, 75, 1, Set.of())); final Score p5s = new Score(p5, ZonedDateTime.now()); - p5s.addDetail(new ScoreResult(s1Key, .8f, 1)); + p5s.addDetail(new ScoreResult(s1Key, 80, 1, Set.of())); final Score p6s = new Score(p6, ZonedDateTime.now()); - p6s.addDetail(new ScoreResult(s1Key, .42f, 1)); + p6s.addDetail(new ScoreResult(s1Key, 42, 1, Set.of())); final Score p7s = new Score(p7, ZonedDateTime.now()); - p7s.addDetail(new ScoreResult(s1Key, 0, 1)); + p7s.addDetail(new ScoreResult(s1Key, 0, 1, Set.of())); - List.of(p1s, p1sOld, p2s, p2sOld, p3s, p4s, p5s, p6s, p7s).forEach(scoreService::save); + Set.of(p1s, p1sOld, p2s, p2sOld, p3s, p4s, p5s, p6s, p7s).forEach(scoreService::save); assertThat(scoreRepository.count()).isEqualTo(9); final ScoreService.ScoreStatistics scoresStatistics = scoreService.getScoresStatistics(); diff --git a/docs/ARCHITECTURE.adoc b/docs/ARCHITECTURE.adoc index e8e6fbfe5..424bd85f0 100644 --- a/docs/ARCHITECTURE.adoc +++ b/docs/ARCHITECTURE.adoc @@ -16,21 +16,21 @@ The probe, when executed, returns a `ProbeResult` which embedded a key, a status * The `key` of the `ProbeResult` is the `key` of the Probe which was executed. * The `message` can provides more details about the probe execution result. -* The `status` can be one of `ResultStatusERROR`, `ResultStatus#FAILURE` or `ResultStatus#SUCCESS`. +* The `status` can be one of `ProbeResult.Status.ERROR` or `ProbeResult.Status.SUCCESS`. * The `timestamp` is used to know when the result was produced. -`ResultStatus#ERROR` is used to mark issues occurring during the probe execution. +`ProbeResult.Status.ERROR` is used to mark issues occurring during the probe execution. It can happen because the requirements to run a probe are not met or because the source of validation cannot be found. -`ResultStatus#FAILURE` is reserved to mark a probe which was executed correctly but the output of the test done by the probe is not up to the standards of the project. -This means that, for example, the configuration file for `dependabot` was not found, or the code coverage is above threshold pre-defined. +`ProbeResult.Status.SUCCESS` means that the probe was executed correctly and the probe could retrieve the data it was looking for. -`ResultStatus#SUCCESS` means that the probe was executed correctly and the output of the probe meant its criteria. +The probes are not making any judgement or observations on the plugin state. +They are only reporting data. Probes are executed by the link:../src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngine.java[`ProbeEngine`]. There are conditions for each probe to be executed: -- if all the requirements defined by the probe were met +- if all the requirements defined by the probe are met - if it has never been executed on a specific plugin - if the probe doesn't require a new release or new code addition to be executed - if the last execution of the probe on a specific plugin was done before the last code change of the plugin and the probe is based on the code of the plugin diff --git a/pom.xml b/pom.xml index 43bc0381d..c08225440 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ io.jenkins.pluginhealth.scoring plugin-health-scoring-parent - 2.8.1-SNAPSHOT + 3.0.0-SNAPSHOT pom Plugin Health Scoring :: Parent diff --git a/test/pom.xml b/test/pom.xml index cf86adaff..8fbd075ee 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -29,7 +29,7 @@ io.jenkins.pluginhealth.scoring plugin-health-scoring-parent - 2.8.1-SNAPSHOT + 3.0.0-SNAPSHOT ../ diff --git a/war/pom.xml b/war/pom.xml index f7f8a6c72..f4caaf4a7 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -29,7 +29,7 @@ io.jenkins.pluginhealth.scoring plugin-health-scoring-parent - 2.8.1-SNAPSHOT + 3.0.0-SNAPSHOT ../ diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ProbesController.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ProbesController.java index 3e02d931f..ef6990fb0 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ProbesController.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ProbesController.java @@ -77,12 +77,11 @@ public ModelAndView listProbeResults() { return modelAndView; } - record ProbeDetails(String id, String description, String[] requirements) { + record ProbeDetails(String id, String description) { static ProbeDetails map(Probe probe) { return new ProbeDetails( probe.key(), - probe.getDescription(), - probe.getProbeResultRequirement() + probe.getDescription() ); } } diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreAPI.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreAPI.java index 5b4d8def6..2db6a7c30 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreAPI.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreAPI.java @@ -24,15 +24,12 @@ package io.jenkins.pluginhealth.scoring.http; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import java.util.stream.Stream; -import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.ScoreResult; import io.jenkins.pluginhealth.scoring.service.ScoreService; -import io.jenkins.pluginhealth.scoring.service.ScoringService; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; @@ -43,11 +40,9 @@ @RequestMapping("/api/scores") public class ScoreAPI { private final ScoreService scoreService; - private final ScoringService scoringService; - public ScoreAPI(ScoreService scoreService, ScoringService scoringService) { + public ScoreAPI(ScoreService scoreService) { this.scoreService = scoreService; - this.scoringService = scoringService; } @GetMapping(value = { "", "/" }, produces = MediaType.APPLICATION_JSON_VALUE) @@ -69,8 +64,16 @@ record Tuple(String name, PluginScoreSummary summary) { ScoreResult::key, scoreResult -> new PluginScoreDetail( scoreResult.value(), - scoreResult.coefficient(), - getScoringComponents(scoreResult, score.getPlugin().getDetails()) + scoreResult.weight(), + scoreResult.componentsResults().stream() + .map(changelogResult -> + new PluginScoreDetailComponent( + changelogResult.score(), + changelogResult.weight(), + changelogResult.reasons() + ) + ) + .collect(Collectors.toList()) ) )) ) @@ -80,35 +83,15 @@ record Tuple(String name, PluginScoreSummary summary) { return new ScoreReport(plugins, stats); } - private Map getScoringComponents(ScoreResult result, - Map probeResults) { - record Tuple(String name, Float value) { - } - - return scoringService.get(result.key()) - .map(scoring -> { - final Map scoreComponents = scoring.getScoreComponents(); - return scoreComponents.entrySet().stream().map(e -> new Tuple(e.getKey(), e.getValue())); - }) - .orElse(Stream.empty()) - .collect(Collectors.toMap( - Tuple::name, - tuple -> new PluginScoreDetailComponent( - probeResults.containsKey(tuple.name()) && probeResults.get(tuple.name()).status().equals(ResultStatus.SUCCESS) ? tuple.value() : 0, - tuple.value() - ) - )); - } - private record ScoreReport(Map plugins, ScoreService.ScoreStatistics statistics) { } private record PluginScoreSummary(long value, Map details) { } - private record PluginScoreDetail(float value, float weight, Map components) { + private record PluginScoreDetail(float value, float weight, List components) { } - private record PluginScoreDetailComponent(float value, float max) { + private record PluginScoreDetailComponent(int value, float weight, List reasons) { } } diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreController.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreController.java index ed2d53dac..8fbe4939a 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreController.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreController.java @@ -24,14 +24,8 @@ package io.jenkins.pluginhealth.scoring.http; -import java.time.ZonedDateTime; -import java.util.Collection; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import io.jenkins.pluginhealth.scoring.model.Plugin; -import io.jenkins.pluginhealth.scoring.service.ProbeService; import io.jenkins.pluginhealth.scoring.service.ScoreService; import io.jenkins.pluginhealth.scoring.service.ScoringService; @@ -46,12 +40,10 @@ @Controller @RequestMapping(path = "/scores") public class ScoreController { - private final ProbeService probeService; private final ScoreService scoreService; private final ScoringService scoringService; - public ScoreController(ProbeService probeService, ScoreService scoreService, ScoringService scoringService) { - this.probeService = probeService; + public ScoreController(ScoreService scoreService, ScoringService scoringService) { this.scoreService = scoreService; this.scoringService = scoringService; } @@ -70,45 +62,10 @@ public ModelAndView list() { public ModelAndView getScoreOf(@PathVariable String pluginName) { return scoreService.latestScoreFor(pluginName) .map(score -> { - final Plugin plugin = score.getPlugin(); - final Map probeResultViews = plugin.getDetails().values().stream() - .map(result -> new ProbeResultView( - result.id(), result.status().name(), result.message() - )) - .collect(Collectors.toMap(ProbeResultView::key, value -> value)); - final List scoreComponents = score.getDetails().stream() - .map(result -> new ScoreResultView(result.key(), result.value(), result.coefficient())) - .toList(); - final Map scores = scoringService.getScoringsView(); - final Map probes = probeService.getProbesView(); - - final PluginScoreView view = new PluginScoreView( - plugin.getName(), - plugin.getScm(), - plugin.getVersion().toString(), - plugin.getReleaseTimestamp(), - score.getValue(), - probeResultViews, - scoreComponents - ); return new ModelAndView("scores/details", Map.of( - "score", view, - "scores", scores, - "probes", probes + "score", score )); }) .orElseGet(() -> new ModelAndView("scores/unknown", Map.of("pluginName", pluginName), HttpStatus.NOT_FOUND)); } - - private record PluginScoreView(String pluginName, String scm, String version, ZonedDateTime releaseTimestamp, - long value, - Map probeResults, - Collection details) { - } - - private record ProbeResultView(String key, String status, String message) { - } - - private record ScoreResultView(String key, float value, float coefficient) { - } } diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngine.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngine.java index 5fa2e344b..a88d14f9f 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngine.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngine.java @@ -28,7 +28,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; import io.jenkins.pluginhealth.scoring.service.PluginDocumentationService; import io.jenkins.pluginhealth.scoring.service.PluginService; @@ -92,7 +91,7 @@ public void runOn(Plugin plugin) throws IOException { private void runOn(Plugin plugin, UpdateCenter updateCenter) { final ProbeContext probeContext; try { - probeContext = probeService.getProbeContext(plugin.getName(), updateCenter); + probeContext = probeService.getProbeContext(plugin, updateCenter); } catch (IOException ex) { LOGGER.error("Cannot create temporary plugin for {}", plugin.getName(), ex); return; @@ -100,12 +99,15 @@ private void runOn(Plugin plugin, UpdateCenter updateCenter) { probeContext.setGitHub(gitHub); probeContext.setPluginDocumentationLinks(pluginDocumentationService.fetchPluginDocumentationUrl()); + probeContext.cloneRepository(); probeService.getProbes().forEach(probe -> { try { final ProbeResult result = probe.apply(plugin, probeContext); - if (result.status() != ResultStatus.ERROR) { - plugin.addDetails(result); + plugin.addDetails(result); + if (ProbeResult.Status.ERROR.equals(result.status())) { + LOGGER.info("There was a problem while running {} on {}", probe.key(), plugin.getName()); + LOGGER.info(result.message()); } } catch (Throwable t) { LOGGER.error("Couldn't run {} on {}", probe.key(), plugin.getName(), t); diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ProbeService.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ProbeService.java index 59fd4de24..2567e608f 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ProbeService.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ProbeService.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.stream.Collectors; +import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; import io.jenkins.pluginhealth.scoring.probes.DependabotPullRequestProbe; import io.jenkins.pluginhealth.scoring.probes.DeprecatedPluginProbe; @@ -89,16 +90,16 @@ private long getProbesRawResultsFromDatabase(String probeID, boolean valid) { }; } - public ProbeContext getProbeContext(String pluginName, UpdateCenter updateCenter) throws IOException { - return new ProbeContext(pluginName, updateCenter); + public ProbeContext getProbeContext(Plugin plugin, UpdateCenter updateCenter) throws IOException { + return new ProbeContext(plugin, updateCenter); } public Map getProbesView() { return getProbes().stream() - .map(probe -> new ProbeView(probe.key(), probe.getDescription(), probe.getProbeResultRequirement())) + .map(probe -> new ProbeView(probe.key(), probe.getDescription())) .collect(Collectors.toMap(ProbeView::key, p -> p)); } - public record ProbeView(String key, String description, String[] requirements) { + public record ProbeView(String key, String description) { } } diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoringService.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoringService.java index dd783db5a..82b37c270 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoringService.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoringService.java @@ -52,10 +52,10 @@ public Optional get(String key) { public Map getScoringsView() { return getScoringList().stream() .map(scoring -> new ScoringService.ScoreView( - scoring.key(), scoring.coefficient(), scoring.description(), scoring.getScoreComponents() + scoring.key(), scoring.weight(), scoring.description() )) .collect(Collectors.toMap(ScoreView::key, s -> s)); } - public record ScoreView(String key, float coefficient, String description, Map components) {} + public record ScoreView(String key, float coefficient, String description) {} } diff --git a/war/src/main/resources/templates/probes/listing.html b/war/src/main/resources/templates/probes/listing.html index 91376c696..e08250f9d 100644 --- a/war/src/main/resources/templates/probes/listing.html +++ b/war/src/main/resources/templates/probes/listing.html @@ -17,18 +17,12 @@

Probes

ID Description - Depends on - - - diff --git a/war/src/main/resources/templates/scores/details.html b/war/src/main/resources/templates/scores/details.html index 352d129bf..8d14714e6 100644 --- a/war/src/main/resources/templates/scores/details.html +++ b/war/src/main/resources/templates/scores/details.html @@ -5,21 +5,21 @@ -
-

+
+

-
+
- +
- +
- +
@@ -34,7 +34,6 @@

Details

Key Value Weight - Description @@ -43,45 +42,27 @@

Details

- - + - +

Details:

- - - + + - - - + + + - @@ -93,30 +74,28 @@

Details:

Probe keyMaximum valueStatusValueWeight Message
- - - - - - Not executed. - This probes requires - - - , - - to be successful to be executed. - +
    +
  • +

Probes results

-
+
- - - + + -
ID Status MessageDescription
- - + +
-

+

No probe were executed on the plugin yet.

diff --git a/war/src/main/resources/templates/scores/listing.html b/war/src/main/resources/templates/scores/listing.html index 312f4b803..935e26e42 100644 --- a/war/src/main/resources/templates/scores/listing.html +++ b/war/src/main/resources/templates/scores/listing.html @@ -13,18 +13,19 @@

Scores

Key Weight - Probe(s) used + What is validated Description - + - +
    +
  • +
diff --git a/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreAPITest.java b/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreAPITest.java index d3c985745..e180b4b52 100644 --- a/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreAPITest.java +++ b/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreAPITest.java @@ -31,17 +31,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.ZonedDateTime; +import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Set; import io.jenkins.pluginhealth.scoring.config.SecurityConfiguration; import io.jenkins.pluginhealth.scoring.model.Plugin; -import io.jenkins.pluginhealth.scoring.model.ProbeResult; import io.jenkins.pluginhealth.scoring.model.Score; import io.jenkins.pluginhealth.scoring.model.ScoreResult; -import io.jenkins.pluginhealth.scoring.scores.Scoring; +import io.jenkins.pluginhealth.scoring.model.ScoringComponentResult; import io.jenkins.pluginhealth.scoring.service.ScoreService; -import io.jenkins.pluginhealth.scoring.service.ScoringService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; @@ -57,67 +56,41 @@ import org.springframework.test.web.servlet.MockMvc; @ExtendWith({ SpringExtension.class, MockitoExtension.class }) -@ImportAutoConfiguration({ProjectInfoAutoConfiguration.class, SecurityConfiguration.class}) +@ImportAutoConfiguration({ ProjectInfoAutoConfiguration.class, SecurityConfiguration.class }) @WebMvcTest( controllers = ScoreAPI.class ) class ScoreAPITest { @MockBean private ScoreService scoreService; - @MockBean private ScoringService scoringService; @Autowired private MockMvc mockMvc; @Autowired ObjectMapper mapper; @Test void shouldBeAbleToProvideScoresSummary() throws Exception { - final String probe1Key = "probe-1", - probe2Key = "probe-2", - probe3Key = "probe-3", - scoring1Key = "scoring-1", - scoring2Key = "scoring-2"; - - final Plugin plugin1 = mock(Plugin.class); - when(plugin1.getDetails()).thenReturn(Map.of( - probe1Key, ProbeResult.success(probe1Key, "") - )); - - final Plugin plugin2 = mock(Plugin.class); - when(plugin2.getDetails()).thenReturn(Map.of( - probe1Key, ProbeResult.success(probe1Key, ""), - probe2Key, ProbeResult.failure(probe2Key, ""), - probe3Key, ProbeResult.failure(probe3Key, "") + final Plugin p1 = mock(Plugin.class); + final Plugin p2 = mock(Plugin.class); + + final Score scoreP1 = new Score(p1, ZonedDateTime.now()); + scoreP1.addDetail(new ScoreResult("scoring-1", 100, 1, Set.of( + new ScoringComponentResult(100, 1, List.of("There is no active security advisory for the plugin.")) + ))); + + final Score scoreP2 = new Score(p2, ZonedDateTime.now()); + scoreP2.addDetail(new ScoreResult("scoring-1", 100, 1, Set.of( + new ScoringComponentResult(100, 1, List.of("There is no active security advisory for the plugin.")) + ))); + scoreP2.addDetail(new ScoreResult("scoring-2", 50, 1, Set.of( + new ScoringComponentResult(0, 1, List.of("There is no Jenkinsfile detected on the plugin repository.")), + new ScoringComponentResult(100, .5f, List.of("The plugin documentation was migrated to its repository.")), + new ScoringComponentResult(100, .5f, List.of("The plugin is using dependabot.", "0 open pull requests from dependency update tool.")) + ))); + + when(scoreService.getLatestScoresSummaryMap()).thenReturn(Map.of( + "plugin-1", scoreP1, + "plugin-2", scoreP2 )); - - final Scoring scoring1 = mock(Scoring.class); - when(scoring1.getScoreComponents()).thenReturn(Map.of( - probe1Key, 1f - )); - when(scoringService.get(scoring1Key)).thenReturn(Optional.of(scoring1)); - - final Scoring scoring2 = mock(Scoring.class); - when(scoring2.getScoreComponents()).thenReturn(Map.of( - probe2Key, 1f, - probe3Key, 1f - )); - when(scoringService.get(scoring2Key)).thenReturn(Optional.of(scoring2)); - - final ScoreResult p1sr1 = new ScoreResult(scoring1Key, 1, 1); - final ScoreResult p2sr1 = new ScoreResult(scoring1Key, 1, 1); - final ScoreResult p2sr2 = new ScoreResult(scoring2Key, 0, 1); - - final Score score1 = new Score(plugin1, ZonedDateTime.now()); - score1.addDetail(p1sr1); - final Score score2 = new Score(plugin2, ZonedDateTime.now()); - score2.addDetail(p2sr1); - score2.addDetail(p2sr2); - - final Map summary = Map.of( - "plugin-1", score1, - "plugin-2", score2 - ); - when(scoreService.getLatestScoresSummaryMap()).thenReturn(summary); - when(scoreService.getScoresStatistics()).thenReturn(new ScoreService.ScoreStatistics( - 50, 0, 100, 100, 100, 100 + 87.5, 50, 100, 100, 100, 100 )); mockMvc.perform(get("/api/scores")) @@ -131,38 +104,54 @@ void shouldBeAbleToProvideScoresSummary() throws Exception { 'value': 100, 'details': { 'scoring-1': { - 'value': 1, + 'value': 100, 'weight': 1, - 'components': { - 'probe-1': { 'value': 1, 'max': 1 } - } + 'components': [{ + 'value': 100, + 'weight': 1, + 'reasons': ['There is no active security advisory for the plugin.'] + }] } } }, 'plugin-2': { - 'value': 50, + 'value': 75, 'details': { 'scoring-1': { - 'value': 1, + 'value': 100, 'weight': 1, - 'components': { - 'probe-1': { 'value': 1, 'max': 1 } - } + 'components': [{ + 'value': 100, + 'weight': 1, + 'reasons': ['There is no active security advisory for the plugin.'] + }] }, 'scoring-2': { - 'value': 0, + 'value': 50, 'weight': 1, - 'components': { - 'probe-2': { 'value': 0, 'max': 1 }, - 'probe-3': { 'value': 0, 'max': 1 } - } + 'components': [{ + 'value': 0, + 'weight': 1, + 'reasons': ['There is no Jenkinsfile detected on the plugin repository.'] + }, { + 'value': 100, + 'weight': .5, + 'reasons': ['The plugin documentation was migrated to its repository.'] + }, { + 'value': 100, + 'weight': .5, + 'reasons': [ + 'The plugin is using dependabot.', + '0 open pull requests from dependency update tool.' + ] + }] } } } }, 'statistics': { - 'average': 50, - 'minimum': 0, + 'average': 87.5, + 'minimum': 50, 'maximum': 100, 'firstQuartile': 100, 'median': 100, diff --git a/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreControllerTest.java b/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreControllerTest.java index 13e39f047..67cb3f92d 100644 --- a/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreControllerTest.java +++ b/war/src/test/java/io/jenkins/pluginhealth/scoring/http/ScoreControllerTest.java @@ -34,6 +34,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import java.time.ZonedDateTime; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -43,7 +44,8 @@ import io.jenkins.pluginhealth.scoring.model.ProbeResult; import io.jenkins.pluginhealth.scoring.model.Score; import io.jenkins.pluginhealth.scoring.model.ScoreResult; -import io.jenkins.pluginhealth.scoring.service.ProbeService; +import io.jenkins.pluginhealth.scoring.scores.Scoring; +import io.jenkins.pluginhealth.scoring.scores.ScoringComponent; import io.jenkins.pluginhealth.scoring.service.ScoreService; import io.jenkins.pluginhealth.scoring.service.ScoringService; @@ -65,13 +67,37 @@ controllers = ScoreController.class ) class ScoreControllerTest { - @MockBean private ProbeService probeService; @MockBean private ScoreService scoreService; @MockBean private ScoringService scoringService; @Autowired private MockMvc mockMvc; @Test void shouldDisplayListOfScoring() throws Exception { + when(scoringService.getScoringList()) + .thenReturn(List.of( + new Scoring() { + @Override + public String key() { + return "foo"; + } + + @Override + public float weight() { + return 1; + } + + @Override + public String description() { + return "description"; + } + + @Override + public List getComponents() { + return List.of(); + } + } + )); + mockMvc.perform(get("/scores")) .andExpect(status().isOk()) .andExpect(view().name("scores/listing")) @@ -88,7 +114,7 @@ void shouldDisplayScoreForSpecificPlugin() throws Exception { final Plugin plugin = mock(Plugin.class); when(plugin.getName()).thenReturn(pluginName); when(plugin.getDetails()).thenReturn(Map.of( - probeKey, ProbeResult.success(probeKey, "message") + probeKey, ProbeResult.success(probeKey, "message", 1) )); when(plugin.getScm()).thenReturn("this-is-the-url-of-the-plugin-location"); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusHours(1)); @@ -98,7 +124,7 @@ void shouldDisplayScoreForSpecificPlugin() throws Exception { when(score.getPlugin()).thenReturn(plugin); when(score.getValue()).thenReturn(42L); when(score.getDetails()).thenReturn(Set.of( - new ScoreResult(scoreKey, .42f, 1f) + new ScoreResult(scoreKey, 42, 1f, Set.of()) )); when(scoreService.latestScoreFor(pluginName)).thenReturn(Optional.of(score)); @@ -107,7 +133,7 @@ void shouldDisplayScoreForSpecificPlugin() throws Exception { .andExpect(status().isOk()) .andExpect(view().name("scores/details")) .andExpect(model().attribute("module", "scores")) - .andExpect(model().attributeExists("score", "scores", "probes")) + .andExpect(model().attributeExists("score")) /*.andExpect(model().attribute( "score", allOf( diff --git a/war/src/test/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngineTest.java b/war/src/test/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngineTest.java index 6e06c2091..bcd636842 100644 --- a/war/src/test/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngineTest.java +++ b/war/src/test/java/io/jenkins/pluginhealth/scoring/probes/ProbeEngineTest.java @@ -26,7 +26,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -36,6 +35,7 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.nio.file.Files; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; @@ -44,7 +44,6 @@ import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; import io.jenkins.pluginhealth.scoring.service.PluginDocumentationService; import io.jenkins.pluginhealth.scoring.service.PluginService; @@ -81,13 +80,13 @@ void shouldBeAbleToRunSimpleProbe() throws IOException { final Probe probe = spy(Probe.class); final ProbeContext ctx = mock(ProbeContext.class); - final ProbeResult expectedResult = ProbeResult.success("foo", "bar"); - - when(plugin.getName()).thenReturn("foo"); + final ProbeResult expectedResult = ProbeResult.success("foo", "bar", 1); + when(plugin.getDetails()).thenReturn(Map.of()); + when(probe.key()).thenReturn("probe"); when(probe.doApply(plugin, ctx)).thenReturn(expectedResult); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -108,12 +107,13 @@ void shouldNotApplyProbeWithReleaseRequirementOnPluginWithNoNewReleaseWithPastRe when(plugin.getName()).thenReturn("foo"); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusDays(1)); - when(plugin.getDetails()).thenReturn(Map.of(probeKey, ProbeResult.success(probeKey, "This is good"))); + when(plugin.getDetails()).thenReturn(Map.of(probeKey, ProbeResult.success(probeKey, "This is good", 1))); when(probe.requiresRelease()).thenReturn(true); when(probe.key()).thenReturn(probeKey); + when(probe.getVersion()).thenReturn(1L); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -131,15 +131,16 @@ void shouldNotApplyProbeRelatedToCodeWithNoNewCode() throws IOException { final ProbeContext ctx = mock(ProbeContext.class); when(plugin.getDetails()).thenReturn(Map.of( - "probe", new ProbeResult("probe", "message", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)) + "probe", new ProbeResult("probe", "message", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1) )); when(plugin.getName()).thenReturn("foo"); + when(probe.getVersion()).thenReturn(1L); when(probe.key()).thenReturn("probe"); when(probe.requiresRelease()).thenReturn(false); when(probe.isSourceCodeRelated()).thenReturn(true); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -154,20 +155,23 @@ void shouldApplyProbeRelatedToCodeWithNewCommit() throws IOException { final Plugin plugin = mock(Plugin.class); final Probe probe = spy(Probe.class); final ProbeContext ctx = mock(ProbeContext.class); - final ProbeResult result = new ProbeResult("probe", "message", ResultStatus.SUCCESS); + final String probeKey = "probe"; + + final ProbeResult result = new ProbeResult(probeKey, "message", ProbeResult.Status.SUCCESS, 1); when(plugin.getDetails()).thenReturn(Map.of( - "probe", new ProbeResult("probe", "message", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)) + probeKey, new ProbeResult(probeKey, "message", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1) )); - when(plugin.getName()).thenReturn("foo"); + when(probe.getVersion()).thenReturn(1L); + when(ctx.getScmRepository()).thenReturn(Optional.of(Files.createTempDirectory("ProbeEngineTest-shouldApplyProbeRelatedToCodeWithNewCommit"))); when(ctx.getLastCommitDate()).thenReturn(Optional.of(ZonedDateTime.now())); - when(probe.key()).thenReturn("probe"); + when(probe.key()).thenReturn(probeKey); when(probe.isSourceCodeRelated()).thenReturn(true); when(probe.doApply(plugin, ctx)).thenReturn(result); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -185,15 +189,15 @@ void shouldApplyProbeWithReleaseRequirementOnPluginWithNewReleaseAndPastResult() final Probe probe = spy(Probe.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getName()).thenReturn("foo"); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now()); - when(plugin.getDetails()).thenReturn(Map.of(probeKey, new ProbeResult(probeKey, "this is ok", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)))); + when(plugin.getDetails()).thenReturn(Map.of(probeKey, new ProbeResult(probeKey, "this is ok", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1))); + when(probe.getVersion()).thenReturn(1L); when(probe.key()).thenReturn(probeKey); when(probe.requiresRelease()).thenReturn(true); - when(probe.doApply(plugin, ctx)).thenReturn(ProbeResult.success(probeKey, "This is also ok")); + when(probe.doApply(plugin, ctx)).thenReturn(ProbeResult.success(probeKey, "This is also ok", 1)); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -211,17 +215,17 @@ void shouldApplyProbeWithNoReleaseRequirementOnPluginWithPastResult() throws IOE final Probe probe = spy(Probe.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getName()).thenReturn("foo"); when(plugin.getDetails()).thenReturn(Map.of( probeKey, - new ProbeResult(probeKey, "this is ok", ResultStatus.SUCCESS, ZonedDateTime.now().minusDays(1)) + new ProbeResult(probeKey, "this is ok", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusDays(1), 1) )); + when(probe.getVersion()).thenReturn(1L); when(probe.requiresRelease()).thenReturn(false); - when(probe.doApply(plugin, ctx)).thenReturn(ProbeResult.success(probeKey, "This is also ok")); + when(probe.doApply(plugin, ctx)).thenReturn(ProbeResult.success(probeKey, "This is also ok", 1)); when(probe.key()).thenReturn(probeKey); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -233,23 +237,21 @@ void shouldApplyProbeWithNoReleaseRequirementOnPluginWithPastResult() throws IOE } @Test - void shouldNotSaveErrors() throws IOException { + void shouldSaveEvenErrors() throws IOException { final Plugin plugin = mock(Plugin.class); final Probe probe = spy(Probe.class); final ProbeContext ctx = mock(ProbeContext.class); - when(plugin.getName()).thenReturn("foo"); - - when(probe.doApply(plugin, ctx)).thenReturn(ProbeResult.error("foo", "bar")); + when(probe.doApply(plugin, ctx)).thenReturn(ProbeResult.error("foo", "bar", 1)); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probe)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); final ProbeEngine probeEngine = new ProbeEngine(probeService, pluginService, updateCenterService, gitHub, pluginDocumentationService); probeEngine.run(); - verify(plugin, never()).addDetails(any(ProbeResult.class)); + verify(plugin).addDetails(any(ProbeResult.class)); } @Test @@ -261,8 +263,8 @@ void shouldBeAbleToGetPreviousContextResultInExecution() throws IOException { @Override protected ProbeResult doApply(Plugin plugin, ProbeContext ctx) { return plugin.getDetails().get("foo") != null ? - ProbeResult.success(key(), "This is also ok") : - ProbeResult.error(key(), "This cannot be validated"); + ProbeResult.success(key(), "This is also ok", this.getVersion()) : + ProbeResult.error(key(), "This cannot be validated", this.getVersion()); } @Override @@ -274,14 +276,17 @@ public String key() { public String getDescription() { return "description"; } - }; - when(plugin.getName()).thenReturn("foo"); + @Override + public long getVersion() { + return 1; + } + }; when(probeOne.key()).thenReturn("foo"); - when(probeOne.doApply(plugin, ctx)).thenReturn(ProbeResult.success("foo", "This is ok")); + when(probeOne.doApply(plugin, ctx)).thenReturn(ProbeResult.success("foo", "This is ok", 1)); - when(probeService.getProbeContext(anyString(), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); when(probeService.getProbes()).thenReturn(List.of(probeOne, probeTwo)); when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); @@ -291,4 +296,57 @@ public String getDescription() { verify(plugin, times(2)).addDetails(any(ProbeResult.class)); assertThat(plugin.getDetails().keySet()).containsExactlyInAnyOrder("foo", "bar"); } + + @Test + void shouldForceProbeExecutionWhenNewVersionOfTheProbe() throws IOException { + final String probeKey = "foo"; + final long version = 1L; + final ProbeResult newProbeResult = new ProbeResult( + probeKey, "this is a message", ProbeResult.Status.SUCCESS, version + 1 + ); + + final Plugin plugin = mock(Plugin.class); + final Probe probe = spy(new Probe() { + @Override + protected ProbeResult doApply(Plugin plugin, ProbeContext context) { + return newProbeResult; + } + + @Override + public String key() { + return probeKey; + } + + @Override + public String getDescription() { + return ""; + } + + @Override + public long getVersion() { + return version + 1; + } + + @Override + protected boolean isSourceCodeRelated() { + return true; + } + }); + final ProbeContext ctx = mock(ProbeContext.class); + + final ProbeResult previousResult = new ProbeResult(probeKey, "this is a message", ProbeResult.Status.SUCCESS, version); + when(plugin.getDetails()).thenReturn(Map.of( + probeKey, previousResult + )); + + when(probeService.getProbeContext(any(Plugin.class), any(UpdateCenter.class))).thenReturn(ctx); + when(probeService.getProbes()).thenReturn(List.of(probe)); + when(pluginService.streamAll()).thenReturn(Stream.of(plugin)); + + final ProbeEngine probeEngine = new ProbeEngine(probeService, pluginService, updateCenterService, gitHub, pluginDocumentationService); + probeEngine.run(); + + verify(probe).doApply(plugin, ctx); + verify(plugin).addDetails(newProbeResult); + } } diff --git a/war/src/test/java/io/jenkins/pluginhealth/scoring/scores/ScoringEngineTest.java b/war/src/test/java/io/jenkins/pluginhealth/scoring/scores/ScoringEngineTest.java index eed8a2e42..1073e6483 100644 --- a/war/src/test/java/io/jenkins/pluginhealth/scoring/scores/ScoringEngineTest.java +++ b/war/src/test/java/io/jenkins/pluginhealth/scoring/scores/ScoringEngineTest.java @@ -37,11 +37,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; import io.jenkins.pluginhealth.scoring.model.Plugin; import io.jenkins.pluginhealth.scoring.model.ProbeResult; -import io.jenkins.pluginhealth.scoring.model.ResultStatus; import io.jenkins.pluginhealth.scoring.model.Score; import io.jenkins.pluginhealth.scoring.model.ScoreResult; import io.jenkins.pluginhealth.scoring.service.PluginService; @@ -68,8 +68,8 @@ void shouldBeAbleToScoreOnePlugin() { final Scoring scoringB = mock(Scoring.class); when(plugin.getName()).thenReturn("foo-bar"); - when(scoringA.apply(plugin)).thenReturn(new ScoreResult("scoring-a", 1, .5f)); - when(scoringB.apply(plugin)).thenReturn(new ScoreResult("scoring-b", 0, 1)); + when(scoringA.apply(plugin)).thenReturn(new ScoreResult("scoring-a", 100, .5f, Set.of())); + when(scoringB.apply(plugin)).thenReturn(new ScoreResult("scoring-b", 0, 1, Set.of())); when(scoringService.getScoringList()).thenReturn(List.of(scoringA, scoringB)); when(scoreService.save(any(Score.class))).then(AdditionalAnswers.returnsFirstArg()); @@ -80,6 +80,7 @@ void shouldBeAbleToScoreOnePlugin() { verify(scoringA).apply(plugin); verify(scoringB).apply(plugin); + assertThat(score).isNotNull(); assertThat(score.getPlugin()).isEqualTo(plugin); assertThat(score.getDetails()).hasSize(2); assertThat(score.getValue()).isEqualTo(33); @@ -98,8 +99,8 @@ void shouldBeAbleToScoreMultiplePlugins() { when(pluginB.getName()).thenReturn("plugin-b"); when(pluginC.getName()).thenReturn("plugin-c"); - when(scoringA.apply(any(Plugin.class))).thenReturn(new ScoreResult("scoring-a", 1, 0.5f)); - when(scoringB.apply(any(Plugin.class))).thenReturn(new ScoreResult("scoring-b", .75f, .75f)); + when(scoringA.apply(any(Plugin.class))).thenReturn(new ScoreResult("scoring-a", 100, 0.5f, Set.of())); + when(scoringB.apply(any(Plugin.class))).thenReturn(new ScoreResult("scoring-b", 75, .75f, Set.of())); when(scoringService.getScoringList()).thenReturn(List.of(scoringA, scoringB)); when(pluginService.streamAll()).thenReturn(Stream.of(pluginA, pluginB, pluginC)); @@ -128,7 +129,7 @@ void shouldOnlyScorePluginsWithNewerResultThanLatestScore() { final String pluginName = "plugin-a"; when(pluginA.getName()).thenReturn(pluginName); when(pluginA.getDetails()).thenReturn(Map.of( - "foo-bar", new ProbeResult("foo-bar", "", ResultStatus.FAILURE, ZonedDateTime.now().minusMinutes(15)) + "foo-bar", new ProbeResult("foo-bar", "", ProbeResult.Status.SUCCESS, ZonedDateTime.now().minusMinutes(15), 1) )); final Scoring scoringA = mock(Scoring.class);