Skip to content

Commit

Permalink
Plugin description migration probe (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
alecharp authored Mar 5, 2024
1 parent 4b9657a commit c3bfdd9
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* MIT License
*
* Copyright (c) 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
* 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.probes;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;

import io.jenkins.pluginhealth.scoring.model.Plugin;
import io.jenkins.pluginhealth.scoring.model.ProbeResult;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(PluginDescriptionMigrationProbe.ORDER)
public class PluginDescriptionMigrationProbe extends Probe {
public static final String KEY = "description-migration";
public static final int ORDER = DocumentationMigrationProbe.ORDER + 100;

@Override
protected ProbeResult doApply(Plugin plugin, ProbeContext context) {
final Optional<Path> scmRepositoryOpt = context.getScmRepository();
if (scmRepositoryOpt.isEmpty()) {
return error("Cannot access plugin repository.");
}

final Path repository = scmRepositoryOpt.get();
final Path pluginFolder = context.getScmFolderPath().map(repository::resolve).orElse(repository);

try (Stream<Path> files = Files.find(
pluginFolder.resolve("src").resolve("main").resolve("resources"),
1,
(path, attributes) -> "index.jelly".equals(path.getFileName().toString())
)) {
final Optional<Path> jellyFileOpt = files.findFirst();
if (jellyFileOpt.isEmpty()) {
return success("There is no `index.jelly` file in `src/main/resources`.");
}
final Path jellyFile = jellyFileOpt.get();
return Files.readAllLines(jellyFile).stream().map(String::trim).anyMatch(s -> s.contains("TODO")) ?
success("Plugin is using description from the plugin archetype.") :
success("Plugin seems to have a correct description.");
} catch (IOException e) {
return error("Cannot browse plugin source code folder.");
}
}

@Override
public String key() {
return KEY;
}

@Override
public String getDescription() {
return "Checks if the plugin description is located in the `src/main/resources/index.jelly` file.";
}

@Override
public long getVersion() {
return 1;
}

@Override
protected boolean requiresRelease() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.jenkins.pluginhealth.scoring.probes.ContinuousDeliveryProbe;
import io.jenkins.pluginhealth.scoring.probes.ContributingGuidelinesProbe;
import io.jenkins.pluginhealth.scoring.probes.DocumentationMigrationProbe;
import io.jenkins.pluginhealth.scoring.probes.PluginDescriptionMigrationProbe;
import io.jenkins.pluginhealth.scoring.probes.ReleaseDrafterProbe;

import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -103,23 +104,22 @@ public ScoringComponentResult getScore(Plugin $, Map<String, ProbeResult> probeR
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."),
List.of(
new Resolution("https://www.jenkins.io/doc/developer/tutorial-improve/migrate-documentation-to-github/")
)
);
case "Documentation is not located in the plugin repository." -> new ScoringComponentResult(
0,
getWeight(),
List.of("Documentation should be migrated in plugin repository."),
List.of(
new Resolution("https://www.jenkins.io/doc/developer/tutorial-improve/migrate-documentation-to-github/")
)
);
default ->
new ScoringComponentResult(0, getWeight(), List.of("Cannot confirm or not the documentation migration.", probeResult.message()));
};
}

@Override
public int getWeight() {
return 8;
return 4;
}
},
new ScoringComponent() {
Expand Down Expand Up @@ -161,6 +161,39 @@ public ScoringComponentResult getScore(Plugin plugin, Map<String, ProbeResult> p
public int getWeight() {
return 0;
}
},
new ScoringComponent() {
@Override
public String getDescription() {
return "Plugin description should be located in the index.jelly file.";
}

@Override
public ScoringComponentResult getScore(Plugin plugin, Map<String, ProbeResult> probeResults) {
final ProbeResult result = probeResults.get(PluginDescriptionMigrationProbe.KEY);
if (result == null || ProbeResult.Status.ERROR.equals(result.status())) {
return new ScoringComponentResult(
0,
getWeight(),
List.of("Cannot determine if the plugin description was correctly migrated.")
);
}

final String message = result.message();
if ("Plugin seems to have a correct description.".equals(message)) {
return new ScoringComponentResult(100, getWeight(), List.of(message));
}
return new ScoringComponentResult(0, getWeight(), List.of(message),
List.of(new Resolution(
"Please see how to migrate the plugin description for the plugin.",
"https://www.jenkins.io/doc/developer/tutorial-improve/move-description-to-index/"
)));
}

@Override
public int getWeight() {
return 4;
}
}
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* MIT License
*
* Copyright (c) 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
* 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.probes;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

import io.jenkins.pluginhealth.scoring.model.Plugin;
import io.jenkins.pluginhealth.scoring.model.ProbeResult;

import org.junit.jupiter.api.Test;

public class PluginDescriptionMigrationProbeTest extends AbstractProbeTest<PluginDescriptionMigrationProbe> {
@Override
PluginDescriptionMigrationProbe getSpy() {
return spy(PluginDescriptionMigrationProbe.class);
}

@Test
void shouldRequireRelease() {
assertThat(getSpy().requiresRelease()).isTrue();
}

@Test
void shouldRequiresPluginRepository() {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "status", "message")
.isEqualTo(ProbeResult.error(PluginDescriptionMigrationProbe.KEY, "Cannot access plugin repository.", 0));
}

@Test
void shouldFailWhenPluginRepositoryIsEmpty() throws Exception {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final Path repository = Files.createTempDirectory(getClass().getSimpleName());
when(ctx.getScmRepository()).thenReturn(Optional.of(repository));

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.error(PluginDescriptionMigrationProbe.KEY, "Cannot browse plugin source code folder.", 0));
}

@Test
void shouldDetectMissingJellyFile() throws Exception {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final Path repository = Files.createTempDirectory(getClass().getSimpleName());
Files.createDirectories(repository.resolve("src").resolve("main").resolve("resources"));
when(ctx.getScmRepository()).thenReturn(Optional.of(repository));

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.success(PluginDescriptionMigrationProbe.KEY, "There is no `index.jelly` file in `src/main/resources`.", 0));
}

@Test
void shouldDetectExampleJellyFile() throws Exception {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final Path repository = Files.createTempDirectory(getClass().getSimpleName());
final Path resources = Files.createDirectories(repository.resolve("src").resolve("main").resolve("resources"));
final Path jelly = Files.createFile(resources.resolve("index.jelly"));
Files.writeString(jelly, """
<div>
TODO
</div>
""");
when(ctx.getScmRepository()).thenReturn(Optional.of(repository));

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.success(PluginDescriptionMigrationProbe.KEY, "Plugin is using description from the plugin archetype.", 0));
}

@Test
void shouldDetectExampleJellyFileWithModule() throws Exception {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final Path repository = Files.createTempDirectory(getClass().getSimpleName());
final Path module = Files.createDirectory(repository.resolve("plugin"));
final Path resources = Files.createDirectories(module.resolve("src").resolve("main").resolve("resources"));
final Path jelly = Files.createFile(resources.resolve("index.jelly"));
Files.writeString(jelly, """
<div>
TODO
</div>
""");
when(ctx.getScmRepository()).thenReturn(Optional.of(repository));
when(ctx.getScmFolderPath()).thenReturn(Optional.of(module));

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.success(PluginDescriptionMigrationProbe.KEY, "Plugin is using description from the plugin archetype.", 0));
}

@Test
void shouldDetectCorrectJellyFile() throws Exception {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final Path repository = Files.createTempDirectory(getClass().getSimpleName());
final Path resources = Files.createDirectories(repository.resolve("src").resolve("main").resolve("resources"));
final Path jelly = Files.createFile(resources.resolve("index.jelly"));
Files.writeString(jelly, """
<div>
This is a plugin doing something.
</div>
""");
when(ctx.getScmRepository()).thenReturn(Optional.of(repository));

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.success(PluginDescriptionMigrationProbe.KEY, "Plugin seems to have a correct description.", 0));
}

@Test
void shouldDetectCorrectJellyFileWithModule() throws Exception {
final Plugin plugin = mock(Plugin.class);
final ProbeContext ctx = mock(ProbeContext.class);

final Path repository = Files.createTempDirectory(getClass().getSimpleName());
final Path module = Files.createDirectory(repository.resolve("plugin"));
final Path resources = Files.createDirectories(module.resolve("src").resolve("main").resolve("resources"));
final Path jelly = Files.createFile(resources.resolve("index.jelly"));
Files.writeString(jelly, """
<div>
This is a plugin doing something.
</div>
""");
when(ctx.getScmRepository()).thenReturn(Optional.of(repository));
when(ctx.getScmFolderPath()).thenReturn(Optional.of(module));

final PluginDescriptionMigrationProbe probe = getSpy();
final ProbeResult result = probe.apply(plugin, ctx);

assertThat(result)
.usingRecursiveComparison()
.comparingOnlyFields("id", "message", "status")
.isEqualTo(ProbeResult.success(PluginDescriptionMigrationProbe.KEY, "Plugin seems to have a correct description.", 0));
}
}
Loading

0 comments on commit c3bfdd9

Please sign in to comment.