From: Julien Lancelot Date: Tue, 16 Jan 2018 12:40:15 +0000 (+0100) Subject: SONAR-10266 Generate quality gate card X-Git-Tag: 7.5~1783 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=4b10c71bb4b2458a908c91980e5046dfa966b9ee;p=sonarqube.git SONAR-10266 Generate quality gate card --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java index 6f605191db2..7b969514073 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java @@ -20,40 +20,83 @@ package org.sonar.server.badge.ws; import com.google.common.io.Resources; -import org.apache.commons.io.IOUtils; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric.Level; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.NewAction; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; -import static java.util.Arrays.asList; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.io.IOUtils.write; +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonarqube.ws.MediaTypes.SVG; public class QualityGateAction implements ProjectBadgesWsAction { - public static final String PARAM_COMPONENT = "component"; - public static final String PARAM_TYPE = "type"; + private static final String PARAM_PROJECT = "project"; + private static final String PARAM_BRANCH = "branch"; + + private final UserSession userSession; + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final SvgGenerator svgGenerator; + + public QualityGateAction(UserSession userSession, DbClient dbClient, ComponentFinder componentFinder, SvgGenerator svgGenerator) { + this.userSession = userSession; + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.svgGenerator = svgGenerator; + } @Override public void define(WebService.NewController controller) { NewAction action = controller.createAction("quality_gate") .setHandler(this) - .setDescription("Generate badge for quality gate as an SVG") + .setSince("7.1") + .setDescription("Generate badge for project's quality gate as an SVG.
" + + "Requires 'Browse' permission on the specified project.") .setResponseExample(Resources.getResource(getClass(), "quality_gate-example.svg")); - action.createParam(PARAM_COMPONENT) + action.createParam(PARAM_PROJECT) .setDescription("Project key") .setRequired(true) .setExampleValue(KEY_PROJECT_EXAMPLE_001); - action.createParam(PARAM_TYPE) - .setDescription("Type of badge.") - .setRequired(false) - .setPossibleValues(asList("BADGE", "CARD")); + action + .createParam(PARAM_BRANCH) + .setDescription("Branch key") + .setExampleValue(KEY_BRANCH_EXAMPLE_001); } @Override public void handle(Request request, Response response) throws Exception { - response.stream().setMediaType("image/svg+xml"); - IOUtils.copy(Resources.getResource(getClass(), "quality_gate-example.svg").openStream(), response.stream().output()); + response.stream().setMediaType(SVG); + String projectKey = request.mandatoryParam(PARAM_PROJECT); + String branch = request.param(PARAM_BRANCH); + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = componentFinder.getByKeyAndOptionalBranch(dbSession, projectKey, branch); + userSession.checkComponentPermission(USER, project); + Level qualityGateStatus = getQualityGate(dbSession, project); + write(svgGenerator.generateQualityGate(qualityGateStatus), response.stream().output(), UTF_8); + } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) { + write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8); + } + } + + private Level getQualityGate(DbSession dbSession, ComponentDto project) { + return Level.valueOf(dbClient.liveMeasureDao().selectMeasure(dbSession, project.uuid(), CoreMetrics.ALERT_STATUS_KEY) + .map(LiveMeasureDto::getTextValue) + .orElseThrow(() -> new ProjectBadgesException(format("Quality gate has not been found for project '%s' and branch '%s'", project.getKey(), project.getBranch())))); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java index 8bf78cd5a8b..bbe633aea05 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java @@ -27,10 +27,14 @@ import java.io.IOException; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.text.StrSubstitutor; +import org.sonar.api.measures.Metric; import org.sonar.api.server.ServerSide; import static java.lang.String.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.sonar.api.measures.Metric.Level.ERROR; +import static org.sonar.api.measures.Metric.Level.OK; +import static org.sonar.api.measures.Metric.Level.WARN; @ServerSide public class SvgGenerator { @@ -51,10 +55,15 @@ public class SvgGenerator { private final String errorTemplate; private final String badgeTemplate; + private final Map qualityGateTemplates; public SvgGenerator() { this.errorTemplate = readTemplate("error.svg"); this.badgeTemplate = readTemplate("badge.svg"); + this.qualityGateTemplates = ImmutableMap.of( + OK, readTemplate("quality_gate_passed.svg"), + WARN, readTemplate("quality_gate_warn.svg"), + ERROR, readTemplate("quality_gate_failed.svg")); } public String generateBadge(String label, String value, Color backgroundValueColor) { @@ -75,6 +84,10 @@ public class SvgGenerator { return strSubstitutor.replace(badgeTemplate); } + public String generateQualityGate(Metric.Level level){ + return qualityGateTemplates.get(level); + } + public String generateError(String error) { Map values = ImmutableMap.of( PARAMETER_TOTAL_WIDTH, valueOf(MARGIN + computeWidth(error) + MARGIN), diff --git a/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_failed.svg b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_failed.svg new file mode 100644 index 00000000000..010e1fef5b7 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_failed.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_passed.svg b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_passed.svg new file mode 100644 index 00000000000..06cba700093 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_passed.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_warn.svg b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_warn.svg new file mode 100644 index 00000000000..f43ca6404a5 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/quality_gate_warn.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_failed.svg b/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_failed.svg new file mode 100644 index 00000000000..010e1fef5b7 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_failed.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_passed.svg b/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_passed.svg new file mode 100644 index 00000000000..06cba700093 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_passed.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_warn.svg b/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_warn.svg new file mode 100644 index 00000000000..c77123455f0 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/sticker/ws/templates/quality_gate_warn.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java index cfecf96178b..56da90807b7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java @@ -19,17 +19,162 @@ */ package org.sonar.server.badge.ws; +import java.io.IOException; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric.Level; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; +import static org.sonar.api.measures.Metric.Level.ERROR; +import static org.sonar.api.measures.Metric.Level.OK; +import static org.sonar.api.measures.Metric.Level.WARN; +import static org.sonar.api.measures.Metric.ValueType.LEVEL; public class QualityGateActionTest { - private WsActionTester ws = new WsActionTester(new QualityGateAction()); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private WsActionTester ws = new WsActionTester( + new QualityGateAction(userSession, db.getDbClient(), new ComponentFinder(db.getDbClient(), null), new SvgGenerator())); + + @Test + public void quality_gate_passed() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + MetricDto metric = createQualityGateMetric(); + db.measures().insertLiveMeasure(project, metric, m -> m.setData(OK.name())); + + String response = ws.newRequest() + .setParam("project", project.getKey()) + .execute().getInput(); + + checkResponse(response, OK); + } + + @Test + public void quality_gate_warn() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + MetricDto metric = createQualityGateMetric(); + db.measures().insertLiveMeasure(project, metric, m -> m.setData(WARN.name())); + + String response = ws.newRequest() + .setParam("project", project.getKey()) + .execute().getInput(); + + checkResponse(response, WARN); + } + + @Test + public void quality_gate_failed() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + MetricDto metric = createQualityGateMetric(); + db.measures().insertLiveMeasure(project, metric, m -> m.setData(ERROR.name())); + + String response = ws.newRequest() + .setParam("project", project.getKey()) + .execute().getInput(); + + checkResponse(response, ERROR); + } + + @Test + public void project_does_not_exist() { + String response = ws.newRequest() + .setParam("project", "unknown") + .execute().getInput(); + + checkError(response, "Component key 'unknown' not found"); + } + + @Test + public void branch_does_not_exist() { + ComponentDto project = db.components().insertMainBranch(); + ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.LONG)); + userSession.addProjectPermission(UserRole.USER, project); + + String response = ws.newRequest() + .setParam("project", branch.getKey()) + .setParam("branch", "unknown") + .execute().getInput(); + + checkError(response, format("Component '%s' on branch 'unknown' not found", branch.getKey())); + } + + @Test + public void fail_on_invalid_quality_gate() { + ComponentDto project = db.components().insertMainBranch(); + userSession.addProjectPermission(UserRole.USER, project); + MetricDto metric = createQualityGateMetric(); + db.measures().insertLiveMeasure(project, metric, m -> m.setData("UNKNOWN")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("No enum constant org.sonar.api.measures.Metric.Level.UNKNOWN"); + + ws.newRequest() + .setParam("project", project.getKey()) + .execute(); + } + + @Test + public void measure_not_found() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + + String response = ws.newRequest() + .setParam("project", project.getKey()) + .execute().getInput(); + + checkError(response, format("Quality gate has not been found for project '%s' and branch 'null'", project.getKey())); + } + + @Test + public void measure_value_is_null() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + MetricDto metric = createQualityGateMetric(); + db.measures().insertLiveMeasure(project, metric, m -> m.setValue(null).setData((String) null)); + + String response = ws.newRequest() + .setParam("project", project.getKey()) + .setParam("metric", metric.getKey()) + .execute().getInput(); + + checkError(response, format("Quality gate has not been found for project '%s' and branch 'null'", project.getKey())); + } + + @Test + public void unauthorized() { + ComponentDto project = db.components().insertPrivateProject(); + + String response = ws.newRequest() + .setParam("project", project.getKey()) + .execute().getInput(); + + checkError(response, "Insufficient privileges"); + } @Test public void test_definition() { @@ -37,20 +182,43 @@ public class QualityGateActionTest { assertThat(def.key()).isEqualTo("quality_gate"); assertThat(def.isInternal()).isFalse(); assertThat(def.isPost()).isFalse(); - assertThat(def.since()).isNull(); + assertThat(def.since()).isEqualTo("7.1"); assertThat(def.responseExampleAsString()).isNotEmpty(); assertThat(def.params()) .extracting(Param::key, Param::isRequired) .containsExactlyInAnyOrder( - tuple("component", true), - tuple("type", false)); + tuple("project", true), + tuple("branch", false)); } - @Test - public void test_example() { - String response = ws.newRequest().execute().getInput(); + private MetricDto createQualityGateMetric() { + return db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY).setValueType(LEVEL.name())); + } + + private void checkError(String svg, String expectedError) { + assertThat(svg).contains("" + expectedError + ""); + } + + private void checkResponse(String response, Level status) { + switch (status) { + case OK: + assertThat(response).isEqualTo(readTemplate("quality_gate_passed.svg")); + break; + case WARN: + assertThat(response).isEqualTo(readTemplate("quality_gate_warn.svg")); + break; + case ERROR: + assertThat(response).isEqualTo(readTemplate("quality_gate_failed.svg")); + break; + } + } - assertThat(response).isEqualTo(ws.getDef().responseExampleAsString()); + private String readTemplate(String template) { + try { + return IOUtils.toString(getClass().getResource("templates/" + template), UTF_8); + } catch (IOException e) { + throw new IllegalStateException(String.format("Can't read svg template '%s'", template), e); + } } }