Skip to content

Commit

Permalink
Add deep links to PR decoration
Browse files Browse the repository at this point in the history
This adds deep links to the three issue types Bugs, Vulnerabilities, and Code Smells.
Furthermore, it adds deep links to Coverage and Duplications.

Fixes #662
  • Loading branch information
marcelstoer authored and mc1arke committed Sep 16, 2023
1 parent 8fed787 commit 78d7725
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup;

import java.util.Set;

public final class Link extends Node {

private static final Set<Class<? extends Node>> SUPPORTED_CHILDREN = Set.of(Text.class, Image.class);
private final String url;

public Link(String url, Node... children) {
super(children);
this.url=url;
}
this.url = url;
}

public String getUrl() {
return url;
}

@Override
boolean isValidChild(Node child) {
return child instanceof Text;
return child != null && SUPPORTED_CHILDREN.contains(child.getClass());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,27 @@ public final class AnalysisSummary {

private final BigDecimal newCoverage;
private final BigDecimal coverage;
private final String coverageUrl;
private final String coverageImageUrl;

private final BigDecimal newDuplications;
private final BigDecimal duplications;
private final String duplicationsUrl;
private final String duplicationsImageUrl;

private final long totalIssueCount;

private final long bugCount;
private final String bugUrl;
private final String bugImageUrl;

private final long securityHotspotCount;
private final long vulnerabilityCount;
private final String vulnerabilityUrl;
private final String vulnerabilityImageUrl;

private final long codeSmellCount;
private final String codeSmellUrl;
private final String codeSmellImageUrl;

private AnalysisSummary(Builder builder) {
Expand All @@ -74,17 +79,22 @@ private AnalysisSummary(Builder builder) {
this.dashboardUrl = builder.dashboardUrl;
this.newCoverage = builder.newCoverage;
this.coverage = builder.coverage;
this.coverageUrl = builder.coverageUrl;
this.coverageImageUrl = builder.coverageImageUrl;
this.newDuplications = builder.newDuplications;
this.duplications = builder.duplications;
this.duplicationsUrl = builder.duplicationsUrl;
this.duplicationsImageUrl = builder.duplicationsImageUrl;
this.totalIssueCount = builder.totalIssueCount;
this.bugCount = builder.bugCount;
this.bugUrl = builder.bugUrl;
this.bugImageUrl = builder.bugImageUrl;
this.securityHotspotCount = builder.securityHotspotCount;
this.vulnerabilityCount = builder.vulnerabilityCount;
this.vulnerabilityUrl = builder.vulnerabilityUrl;
this.vulnerabilityImageUrl = builder.vulnerabilityImageUrl;
this.codeSmellCount = builder.codeSmellCount;
this.codeSmellUrl = builder.codeSmellUrl;
this.codeSmellImageUrl = builder.codeSmellImageUrl;
}

Expand Down Expand Up @@ -120,6 +130,10 @@ public BigDecimal getCoverage() {
return coverage;
}

public String getCoverageUrl() {
return coverageUrl;
}

public String getCoverageImageUrl() {
return coverageImageUrl;
}
Expand All @@ -132,6 +146,10 @@ public BigDecimal getDuplications() {
return duplications;
}

public String getDuplicationsUrl() {
return duplicationsUrl;
}

public String getDuplicationsImageUrl() {
return duplicationsImageUrl;
}
Expand All @@ -144,6 +162,10 @@ public long getBugCount() {
return bugCount;
}

public String getBugUrl() {
return bugUrl;
}

public String getBugImageUrl() {
return bugImageUrl;
}
Expand All @@ -156,6 +178,10 @@ public long getVulnerabilityCount() {
return vulnerabilityCount;
}

public String getVulnerabilityUrl() {
return vulnerabilityUrl;
}

public String getVulnerabilityImageUrl() {
return vulnerabilityImageUrl;
}
Expand All @@ -164,6 +190,10 @@ public long getCodeSmellCount() {
return codeSmellCount;
}

public String getCodeSmellUrl() {
return codeSmellUrl;
}

public String getCodeSmellImageUrl() {
return codeSmellImageUrl;
}
Expand All @@ -185,26 +215,26 @@ public String format(FormatterFactory formatterFactory) {
new Heading(2, new Text(pluralOf(getTotalIssueCount(), "Issue", "Issues"))),
new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List(
com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET,
new ListItem(new Image("Bug", getBugImageUrl()),
new ListItem(new Link(getBugUrl(), new Image("Bug", getBugImageUrl())),
new Text(" "),
new Text(pluralOf(getBugCount(), "Bug", "Bugs"))),
new ListItem(new Image("Vulnerability", getVulnerabilityImageUrl()),
new ListItem(new Link(getVulnerabilityUrl(), new Image("Vulnerability", getVulnerabilityImageUrl())),
new Text(" "),
new Text(pluralOf(getVulnerabilityCount() + getSecurityHotspotCount(), "Vulnerability", "Vulnerabilities"))),
new ListItem(new Image("Code Smell", getCodeSmellImageUrl()),
new ListItem(new Link(getCodeSmellUrl(), new Image("Code Smell", getCodeSmellImageUrl())),
new Text(" "),
new Text(pluralOf(getCodeSmellCount(), "Code Smell", "Code Smells")))),
new Heading(2, new Text("Coverage and Duplications")),
new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List(
com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET,
new ListItem(new Image("Coverage", getCoverageImageUrl()),
new ListItem(new Link(getCoverageUrl(), new Image("Coverage", getCoverageImageUrl())),
new Text(" "), new Text(
Optional.ofNullable(getNewCoverage())
.map(decimalFormat::format)
.map(i -> i + "% Coverage")
.orElse("No coverage information") + " (" +
decimalFormat.format(Optional.ofNullable(getCoverage()).orElse(BigDecimal.valueOf(0))) + "% Estimated after merge)")),
new ListItem(new Image("Duplications", getDuplicationsImageUrl()),
new ListItem(new Link(getDuplicationsUrl(), new Image("Duplications", getDuplicationsImageUrl())),
new Text(" "),
new Text(Optional.ofNullable(getNewDuplications())
.map(decimalFormat::format)
Expand Down Expand Up @@ -236,22 +266,27 @@ public static class Builder {

private BigDecimal newCoverage;
private BigDecimal coverage;
private String coverageUrl;
private String coverageImageUrl;

private BigDecimal newDuplications;
private BigDecimal duplications;
private String duplicationsUrl;
private String duplicationsImageUrl;

private long totalIssueCount;

private long bugCount;
private String bugUrl;
private String bugImageUrl;

private long securityHotspotCount;
private long vulnerabilityCount;
private String vulnerabilityUrl;
private String vulnerabilityImageUrl;

private long codeSmellCount;
private String codeSmellUrl;
private String codeSmellImageUrl;

private Builder() {
Expand Down Expand Up @@ -298,6 +333,11 @@ public Builder withCoverage(BigDecimal coverage) {
return this;
}

public Builder withCoverageUrl(String coverageUrl) {
this.coverageUrl = coverageUrl;
return this;
}

public Builder withCoverageImageUrl(String coverageImageUrl) {
this.coverageImageUrl = coverageImageUrl;
return this;
Expand All @@ -313,6 +353,11 @@ public Builder withDuplications(BigDecimal duplications) {
return this;
}

public Builder withDuplicationsUrl(String duplicationsUrl) {
this.duplicationsUrl = duplicationsUrl;
return this;
}

public Builder withDuplicationsImageUrl(String duplicationsImageUrl) {
this.duplicationsImageUrl = duplicationsImageUrl;
return this;
Expand All @@ -328,6 +373,11 @@ public Builder withBugCount(long bugCount) {
return this;
}

public Builder withBugUrl(String bugUrl) {
this.bugUrl = bugUrl;
return this;
}

public Builder withBugImageUrl(String bugImageUrl) {
this.bugImageUrl = bugImageUrl;
return this;
Expand All @@ -343,6 +393,11 @@ public Builder withVulnerabilityCount(long vulnerabilityCount) {
return this;
}

public Builder withVulnerabilityUrl(String vulnerabilityUrl) {
this.vulnerabilityUrl = vulnerabilityUrl;
return this;
}

public Builder withVulnerabilityImageUrl(String vulnerabilityImageUrl) {
this.vulnerabilityImageUrl = vulnerabilityImageUrl;
return this;
Expand All @@ -353,6 +408,11 @@ public Builder withCodeSmellCount(long codeSmellCount) {
return this;
}

public Builder withCodeSmellUrl(String codeSmellUrl) {
this.codeSmellUrl = codeSmellUrl;
return this;
}

public Builder withCodeSmellImageUrl(String codeSmellImageUrl) {
this.codeSmellImageUrl = codeSmellImageUrl;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,18 @@ public AnalysisSummary createAnalysisSummary(AnalysisDetails analysisDetails) {
.withProjectKey(analysisDetails.getAnalysisProjectKey())
.withSummaryImageUrl(baseImageUrl + "/common/icon.png")
.withBugCount(issueCounts.get(RuleType.BUG))
.withBugUrl(getIssuesUrlForRuleType(analysisDetails, RuleType.BUG))
.withBugImageUrl(baseImageUrl + "/common/bug.svg?sanitize=true")
.withCodeSmellCount(issueCounts.get(RuleType.CODE_SMELL))
.withCodeSmellUrl(getIssuesUrlForRuleType(analysisDetails, RuleType.CODE_SMELL))
.withCodeSmellImageUrl(baseImageUrl + "/common/code_smell.svg?sanitize=true")
.withCoverage(coverage)
.withNewCoverage(newCoverage)
.withCoverageUrl(getComponentMeasuresUrlForCodeMetrics(analysisDetails, CoreMetrics.NEW_COVERAGE_KEY))
.withCoverageImageUrl(createCoverageImage(newCoverage, baseImageUrl))
.withDashboardUrl(getDashboardUrl(analysisDetails))
.withDuplications(duplications)
.withDuplicationsUrl(getComponentMeasuresUrlForCodeMetrics(analysisDetails, CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY))
.withDuplicationsImageUrl(createDuplicateImage(newDuplications, baseImageUrl))
.withNewDuplications(newDuplications)
.withFailedQualityGateConditions(failedConditions.stream()
Expand All @@ -148,12 +152,31 @@ public AnalysisSummary createAnalysisSummary(AnalysisDetails analysisDetails) {
? baseImageUrl + "/checks/QualityGateBadge/passed.svg?sanitize=true"
: baseImageUrl + "/checks/QualityGateBadge/failed.svg?sanitize=true")
.withTotalIssueCount(issueTotal)
.withVulnerabilityCount(issueCounts.get(RuleType.VULNERABILITY))
.withSecurityHotspotCount(issueCounts.get(RuleType.SECURITY_HOTSPOT))
.withVulnerabilityCount(issueCounts.get(RuleType.VULNERABILITY))
.withVulnerabilityUrl(getIssuesUrlForRuleType(analysisDetails, RuleType.VULNERABILITY))
.withVulnerabilityImageUrl(baseImageUrl + "/common/vulnerability.svg?sanitize=true")
.build();
}

private String getIssuesUrlForRuleType(AnalysisDetails analysisDetails, RuleType ruleType) {
// https://my-server:port/project/issues?pullRequest=341&resolved=false&types=BUG&inNewCodePeriod=true&id=some-key
return server.getPublicRootUrl() +
"/project/issues?pullRequest=" + analysisDetails.getPullRequestId() +
"&resolved=false&types=" + ruleType.name() +
"&inNewCodePeriod=true" +
"&id=" + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8);
}

private String getComponentMeasuresUrlForCodeMetrics(AnalysisDetails analysisDetails, String codeMetricsKey) {
// https://my-server:port/component_measures?id=some-key&metric=new_coverage&pullRequest=341&view=list
return server.getPublicRootUrl() +
"/component_measures?id=" + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8) +
"&metric=" + codeMetricsKey +
"&pullRequest=" + analysisDetails.getPullRequestId() +
"&view=list";
}

private String getBaseImageUrl() {
return configuration.get(CommunityBranchPlugin.IMAGE_URL_BASE)
.orElse(server.getPublicRootUrl() + "/static/communityBranchPlugin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,62 @@
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup;

import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.junit.jupiter.params.provider.Arguments.arguments;

public class LinkTest {
class LinkTest {

@Test
public void correctParametersReturned() {
Link image = new Link("url", new Text("Text"));
assertThat(image).extracting(Link::getUrl).isEqualTo("url");
}
private final String testUrl = "url";
private final Link link = new Link(testUrl, new Text("Text"));

@Test
public void testIsValidChildInvalidChild() {
assertFalse(new Link("url", new Text("Text")).isValidChild(new Paragraph()));
}
@Nested
class Constructor {
@Test
void shouldCorrectlyAssignParameters() {
// given link under test

@Test
public void testIsValidChildValidChildText() {
assertTrue(new Link("url", new Text("Text")).isValidChild(new Text("")));
// when
String url = link.getUrl();

// then
assertThat(url).isEqualTo(testUrl);
}
}

}
@Nested
@TestInstance(PER_CLASS)
class IsValid {

@MethodSource("childNodes")
@ParameterizedTest(name = "child type: {0} => {1}")
void shouldReturnTrueForSupportedChildren(Node child, boolean expectedResult) {
// given link under test and parameters

// when
boolean validChild = link.isValidChild(child);

// then
assertThat(validChild).isEqualTo(expectedResult);
}

private Stream<Arguments> childNodes() {
return Stream.of(
arguments(named(Text.class.getSimpleName(), new Text("")), true),
arguments(named(Image.class.getSimpleName(), new Image("alt", "source")), true),
arguments(named(Paragraph.class.getSimpleName(), new Paragraph()), false),
arguments(named("null", null), false)
);
}
}
}
Loading

0 comments on commit 78d7725

Please sign in to comment.