Skip to content

Commit

Permalink
Provides API to get plugins scores (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
alecharp committed Jan 13, 2023
1 parent 7842505 commit 34a63cc
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 3 deletions.
49 changes: 49 additions & 0 deletions src/main/java/io/jenkins/pluginhealth/scoring/http/ScoreAPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* MIT License
*
* Copyright (c) 2022 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.http;

import java.util.Map;

import io.jenkins.pluginhealth.scoring.service.ScoreService;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/scores")
public class ScoreAPI {
private final ScoreService scoreService;

public ScoreAPI(ScoreService scoreService) {
this.scoreService = scoreService;
}

@GetMapping(value = {"", "/"}, produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, ScoreService.ScoreSummary> all() {
return scoreService.getLatestScoresSummaryMap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public ScoreController(ScoreService scoreService, ScoringService scoringService)
}

@ModelAttribute(name = "module")
/* default */ String module() {
/* default */ String module() {
return "scores";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,28 @@

package io.jenkins.pluginhealth.scoring.repository;

import java.util.List;
import java.util.Optional;

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

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface ScoreRepository extends JpaRepository<Score, Long> {
Optional<Score> findFirstByPluginOrderByComputedAtDesc(Plugin plugin);

@Query(
value = """
SELECT DISTINCT ON (s.plugin_id)
s.plugin_id, s.value, s.computed_at, s.details, s.id
FROM scores s JOIN plugins p on s.plugin_id = p.id
ORDER BY s.plugin_id, s.computed_at DESC;
""",
nativeQuery = true
)
List<Score> findLatestScoreForAllPlugins();
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public PluginService(PluginRepository pluginRepository) {
}

@Transactional
public Plugin saveOrUpdate(Plugin plugin) {
return pluginRepository.findByName(plugin.getName())
public void saveOrUpdate(Plugin plugin) {
pluginRepository.findByName(plugin.getName())
.map(pluginFromDatabase -> pluginFromDatabase
.setScm(plugin.getScm())
.setReleaseTimestamp(plugin.getReleaseTimestamp())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@

package io.jenkins.pluginhealth.scoring.service;

import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import io.jenkins.pluginhealth.scoring.model.Score;
import io.jenkins.pluginhealth.scoring.model.ScoreResult;
import io.jenkins.pluginhealth.scoring.repository.ScoreRepository;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ScoreService {
Expand All @@ -45,8 +51,21 @@ public Score save(Score score) {
return repository.save(score);
}

@Transactional(readOnly = true)
public Optional<Score> latestScoreFor(String pluginName) {
return pluginService.findByName(pluginName)
.flatMap(repository::findFirstByPluginOrderByComputedAtDesc);
}

@Transactional(readOnly = true)
public Map<String, ScoreSummary> getLatestScoresSummaryMap() {
return repository.findLatestScoreForAllPlugins().stream()
.collect(Collectors.toMap(
score -> score.getPlugin().getName(),
score -> new ScoreSummary(score.getValue(), score.getPlugin().getVersion().toString(), score.getDetails(), score.getComputedAt())
));
}

public record ScoreSummary(long value, String version, Set<ScoreResult> details, ZonedDateTime timestamp) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.http;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Set;

import io.jenkins.pluginhealth.scoring.model.ScoreResult;
import io.jenkins.pluginhealth.scoring.service.ScoreService;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

@ExtendWith({SpringExtension.class, MockitoExtension.class})
@ImportAutoConfiguration(ProjectInfoAutoConfiguration.class)
@WebMvcTest(
controllers = ScoreAPI.class
)
public class ScoreAPITest {
@MockBean private ScoreService scoreService;
@Autowired private MockMvc mockMvc;
@Autowired ObjectMapper mapper;

@Test
public void shouldBeAbleToProvideScoresSummary() throws Exception {
final ScoreResult p1sr1 = new ScoreResult("foo", 1, 1);
final ScoreResult p2sr1 = new ScoreResult("foo", 1, 1);
final ScoreResult p2sr2 = new ScoreResult("bar", 0, .69f);
final ScoreResult p2sr3 = new ScoreResult("wiz", 0, .69f);

final Map<String, ScoreService.ScoreSummary> summary = Map.of(
"plugin-1", new ScoreService.ScoreSummary(100, "1.0", Set.of(p1sr1), ZonedDateTime.now().minusMinutes(2)),
"plugin-2", new ScoreService.ScoreSummary(42, "2.0", Set.of(p2sr1, p2sr2, p2sr3), ZonedDateTime.now().minusMinutes(2))
);
when(scoreService.getLatestScoresSummaryMap()).thenReturn(summary);

mockMvc.perform(get("/api/scores"))
.andExpect(status().isOk())
.andExpectAll(
content().contentType(MediaType.APPLICATION_JSON),
content().json(mapper.writeValueAsString(summary))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;

import io.jenkins.pluginhealth.scoring.AbstractDBContainerTest;
import io.jenkins.pluginhealth.scoring.model.Plugin;
import io.jenkins.pluginhealth.scoring.model.Score;
import io.jenkins.pluginhealth.scoring.model.ScoreResult;
import io.jenkins.pluginhealth.scoring.repository.ScoreRepository;

import hudson.util.VersionNumber;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.boot.test.mock.mockito.MockBean;

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
public class ScoreServiceIT extends AbstractDBContainerTest {
@Autowired private TestEntityManager entityManager;
@Autowired private ScoreRepository scoreRepository;
@MockBean private PluginService pluginService;

@Test
public void shouldBeEmpty() {
assertThat(scoreRepository.count()).isZero();
}

@Test
public void shouldBeAbleToSaveScoreForPlugin() {
final Plugin p1 = entityManager.persist(
new Plugin("plugin-1", null, null, ZonedDateTime.now().minusMinutes(5))
);

final Score score = new Score(p1, ZonedDateTime.now());
final ScoreResult result = new ScoreResult("foo", 1, 1);
score.addDetail(result);

final Score saved = scoreRepository.save(score);
assertThat(saved)
.extracting(Score::getPlugin, Score::getValue)
.contains(p1, 100L);
assertThat(saved.getDetails())
.hasSize(1)
.contains(result);
}

@Test
public void shouldBeAbleToExtractScoreSummary() {
final Plugin p1 = entityManager.persist(
new Plugin("plugin-1", new VersionNumber("1.0"), null, ZonedDateTime.now().minusMinutes(5))
);
final Plugin p2 = entityManager.persist(
new Plugin("plugin-2", new VersionNumber("2.0"), "scm", ZonedDateTime.now().minusMinutes(10))
);

final Score p1s = new Score(p1, ZonedDateTime.now());
p1s.addDetail(new ScoreResult("foo", 1, 1));
p1s.addDetail(new ScoreResult("bar", 0, .5f));

final Score p2s = new Score(p2, ZonedDateTime.now());
p2s.addDetail(new ScoreResult("foo", 0, 1));

scoreRepository.saveAll(List.of(p1s, p2s));
assertThat(scoreRepository.count()).isEqualTo(2);

final ScoreService scoreService = new ScoreService(scoreRepository, pluginService);
final Map<String, ScoreService.ScoreSummary> summary = scoreService.getLatestScoresSummaryMap();

assertThat(summary)
.extractingFromEntries(
Map.Entry::getKey,
Map.Entry::getValue
)
.containsExactlyInAnyOrder(
tuple(
p1.getName(),
new ScoreService.ScoreSummary(p1s.getValue(), p1.getVersion().toString(), p1s.getDetails(), p1s.getComputedAt())
),
tuple(
p2.getName(),
new ScoreService.ScoreSummary(p2s.getValue(), p2.getVersion().toString(), p2s.getDetails(), p2s.getComputedAt())
)
);
}

@Test
public void shouldOnlyRetrieveLatestScoreForPlugins() {

final Plugin p1 = entityManager.persist(
new Plugin("plugin-1", new VersionNumber("1.0"), null, ZonedDateTime.now().minusMinutes(5))
);
final Plugin p2 = entityManager.persist(
new Plugin("plugin-2", new VersionNumber("2.0"), "scm", ZonedDateTime.now().minusMinutes(10))
);


final Score p1s = new Score(p1, ZonedDateTime.now());
p1s.addDetail(new ScoreResult("foo", 1, 1));
p1s.addDetail(new ScoreResult("bar", 0, .5f));

final Score p2s = new Score(p2, ZonedDateTime.now());
p2s.addDetail(new ScoreResult("foo", 0, 1));

final Score p1sOld = new Score(p1, ZonedDateTime.now().minusMinutes(10));
p1sOld.addDetail(new ScoreResult("foo", 1, 1));
p1sOld.addDetail(new ScoreResult("bar", 0, .5f));

final Score p1sOld2 = new Score(p1, ZonedDateTime.now().minusMinutes(15));
p1sOld2.addDetail(new ScoreResult("foo", 1, 1));
p1sOld2.addDetail(new ScoreResult("bar", 0, .5f));

final Score p2sOld = new Score(p2, ZonedDateTime.now().minusMinutes(10));
p2sOld.addDetail(new ScoreResult("foo", 0, 1));

scoreRepository.saveAll(List.of(p1s, p2s, p1sOld, p2sOld, p1sOld2));
assertThat(scoreRepository.count()).isEqualTo(5);

final ScoreService scoreService = new ScoreService(scoreRepository, pluginService);
final Map<String, ScoreService.ScoreSummary> summary = scoreService.getLatestScoresSummaryMap();

assertThat(summary)
.extractingFromEntries(
Map.Entry::getKey,
Map.Entry::getValue
)
.containsExactlyInAnyOrder(
tuple(
p1.getName(),
new ScoreService.ScoreSummary(p1s.getValue(), p1.getVersion().toString(), p1s.getDetails(), p1s.getComputedAt())
),
tuple(
p2.getName(),
new ScoreService.ScoreSummary(p2s.getValue(), p2.getVersion().toString(), p2s.getDetails(), p2s.getComputedAt())
)
);
}
}

0 comments on commit 34a63cc

Please sign in to comment.