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 f33a81c5a..66191ed69 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 @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023-2024 Jenkins Infra + * Copyright (c) 2022-2024 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 @@ -21,7 +21,6 @@ * 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.time.Duration; @@ -48,10 +47,9 @@ public class AdoptionScoring extends Scoring { private abstract static class TimeSinceLastCommitScoringComponent implements ScoringComponent { protected 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 ZonedDateTime commitDate = ZonedDateTime.parse(lastCommitDateMessage, DateTimeFormatter.ISO_DATE_TIME) + .withZoneSameInstant(getZone()); + return Duration.between(then, commitDate); } protected ZoneId getZone() { @@ -67,70 +65,101 @@ public int getWeight() { @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.")); + new ScoringComponent() { + @Override + public String getDescription() { + return "The plugin must not be marked as 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."), - List.of( - new Resolution("See adoption guidelines", "https://www.jenkins.io/doc/developer/plugin-governance/adopt-a-plugin/#plugins-marked-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 a reasonable time gap between last release and last commit."; - } - - @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(); - final String defaultReason = "There are %d days between last release and last commit.".formatted(days); - if (days < Duration.of(30 * 6, ChronoUnit.DAYS).toDays()) { - return new ScoringComponentResult(100, getWeight(), List.of(defaultReason, "Less than 6 months gap between last release and last commit.")); + @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."), + List.of( + new Resolution( + "See adoption guidelines", + "https://www.jenkins.io/doc/developer/plugin-governance/adopt-a-plugin/#plugins-marked-for-adoption"))); + default -> new ScoringComponentResult(-100, getWeight(), List.of()); + }; } - if (days < Duration.of((30 * 12) + 1, ChronoUnit.DAYS).toDays()) { - return new ScoringComponentResult(60, getWeight(), List.of(defaultReason, "Less than a year between last release and last commit.")); + + @Override + public int getWeight() { + return 1; } - if (days < Duration.of((30 * 12 * 2) + 1, ChronoUnit.DAYS).toDays()) { - return new ScoringComponentResult(20, getWeight(), List.of(defaultReason, "Less than 2 years between last release and last commit.")); + }, + new TimeSinceLastCommitScoringComponent() { + @Override + public String getDescription() { + return "There must be a reasonable time gap between last release and last commit."; } - if (days < Duration.of((30 * 12 * 4) + 1, ChronoUnit.DAYS).toDays()) { - return new ScoringComponentResult(10, 2, List.of(defaultReason, "Less than 4 years between last release and last commit.")); + + @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 < 0) { + return new ScoringComponentResult( + 100, + getWeight(), + List.of("The latest release is more recent than the latest commit on the plugin.")); + } + final String defaultReason = + "There are %d days between last release and last commit.".formatted(days); + if (days < Duration.of(30 * 6, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult( + 100, + getWeight(), + List.of( + defaultReason, + "Less than 6 months gap between last release and last commit.")); + } + if (days < Duration.of((30 * 12) + 1, ChronoUnit.DAYS).toDays()) { + return new ScoringComponentResult( + 60, + getWeight(), + List.of(defaultReason, "Less than a year between last release and last commit.")); + } + if (days + < Duration.of((30 * 12 * 2) + 1, ChronoUnit.DAYS) + .toDays()) { + return new ScoringComponentResult( + 20, + getWeight(), + List.of(defaultReason, "Less than 2 years between last release and last commit.")); + } + if (days + < Duration.of((30 * 12 * 4) + 1, ChronoUnit.DAYS) + .toDays()) { + return new ScoringComponentResult( + 10, + 2, + List.of(defaultReason, "Less than 4 years between last release and last commit.")); + } + return new ScoringComponentResult( + -1000, + getWeight(), + List.of("There is more than 4 years between the last release and the last commit.")); } - return new ScoringComponentResult(-1000, getWeight(), List.of("There is more than 4 years between the last release and the last commit.")); - } - } - ); + }); } @Override @@ -150,6 +179,6 @@ public String description() { @Override public int version() { - return 4; + return 5; } } 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 636387fdc..f3b004522 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 @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2023 Jenkins Infra + * Copyright (c) 2022-2024 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 @@ -21,7 +21,6 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - package io.jenkins.pluginhealth.scoring.scores; import static org.assertj.core.api.Assertions.assertThat; @@ -52,16 +51,17 @@ void shouldScoreZeroForPluginsUpForAdoption() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); - when(plugin.getDetails()).thenReturn(Map.of( - UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is up for adoption.", 1) - )); + 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.weight()).isEqualTo(.8f); assertThat(result.value()).isEqualTo(0); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("Cannot determine the last commit date."); + .contains("Cannot determine the last commit date."); } @Test @@ -70,10 +70,15 @@ void shouldScoreZeroForPluginsUpForAdoptionEvenWithRecentCommit() { 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) - )); + 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"); @@ -86,14 +91,15 @@ void shouldScoreZeroForPluginsWithNoLastCommit() { final AdoptionScoring scoring = getSpy(); final Plugin plugin = mock(Plugin.class); - when(plugin.getDetails()).thenReturn( - Map.of(UpForAdoptionProbe.KEY, ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1)) - ); + when(plugin.getDetails()) + .thenReturn(Map.of( + UpForAdoptionProbe.KEY, + ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1))); final ScoreResult result = scoring.apply(plugin); assertThat(result.value()).isEqualTo(0); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("Cannot determine the last commit date."); + .contains("Cannot determine the last commit date."); } @Test @@ -102,17 +108,20 @@ void shouldScoreHundredForPluginsWithCommitsLessThanSixMonthsOld() { final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusMonths(2)); - when(plugin.getDetails()).thenReturn( - Map.of( - 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) - ) - ); + when(plugin.getDetails()) + .thenReturn(Map.of( + 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(100); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("Less than 6 months gap between last release and last commit."); + .contains("Less than 6 months gap between last release and last commit."); } @Test @@ -121,17 +130,20 @@ void shouldScoreEightyForPluginsWithCommitsLessThanOneYearOld() { final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusMonths(8)); - when(plugin.getDetails()).thenReturn( - Map.of( - 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) - ) - ); + when(plugin.getDetails()) + .thenReturn(Map.of( + 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(80); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("Less than a year between last release and last commit."); + .contains("Less than a year between last release and last commit."); } @Test @@ -140,17 +152,20 @@ void shouldScoreSixtyForPluginsWithCommitsLessThanTwoYearsOld() { final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusMonths(18)); - when(plugin.getDetails()).thenReturn( - Map.of( - 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) - ) - ); + when(plugin.getDetails()) + .thenReturn(Map.of( + 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(60); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("Less than 2 years between last release and last commit."); + .contains("Less than 2 years between last release and last commit."); } @Test @@ -159,17 +174,20 @@ void shouldScoreFortyForPluginsWithCommitsLessThanFourYearsOld() { final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusYears(3)); - when(plugin.getDetails()).thenReturn( - Map.of( - 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) - ) - ); + when(plugin.getDetails()) + .thenReturn(Map.of( + 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(40); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("Less than 4 years between last release and last commit."); + .contains("Less than 4 years between last release and last commit."); } @Test @@ -178,16 +196,44 @@ void shouldScoreZeroForPluginsWithCommitsMoreThanFourYearsOld() { final Plugin plugin = mock(Plugin.class); when(plugin.getReleaseTimestamp()).thenReturn(ZonedDateTime.now().minusYears(5)); - when(plugin.getDetails()).thenReturn( - Map.of( - 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) - ) - ); + when(plugin.getDetails()) + .thenReturn(Map.of( + 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(0); assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) - .contains("There is more than 4 years between the last release and the last commit."); + .contains("There is more than 4 years between the last release and the last commit."); + } + + @Test + void shouldNotHaveProblemWithMoreRecentReleaseThanCommit() throws Exception { + final ZonedDateTime commitDateTime = ZonedDateTime.now().minusYears(2); + final ZonedDateTime releaseDateTime = ZonedDateTime.now().minusMinutes(10); + + final AdoptionScoring scoring = getSpy(); + final Plugin plugin = mock(Plugin.class); + + when(plugin.getReleaseTimestamp()).thenReturn(releaseDateTime); + when(plugin.getDetails()) + .thenReturn(Map.of( + UpForAdoptionProbe.KEY, + ProbeResult.success(UpForAdoptionProbe.KEY, "This plugin is not up for adoption.", 1), + LastCommitDateProbe.KEY, + ProbeResult.success( + LastCommitDateProbe.KEY, + commitDateTime.format(DateTimeFormatter.ISO_DATE_TIME), + 1))); + + final ScoreResult result = scoring.apply(plugin); + assertThat(result.value()).isEqualTo(100); + assertThat(result.componentsResults().stream().flatMap(scr -> scr.reasons().stream())) + .contains("The latest release is more recent than the latest commit on the plugin."); } }