diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java index dafd10825..aaf321bc3 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java @@ -18,14 +18,17 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; +import java.util.Set; + public final class Link extends Node { + private static final Set> 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; @@ -33,6 +36,6 @@ public String getUrl() { @Override boolean isValidChild(Node child) { - return child instanceof Text; + return child != null && SUPPORTED_CHILDREN.contains(child.getClass()); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java index a95f913ac..a3468b256 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java @@ -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) { @@ -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; } @@ -120,6 +130,10 @@ public BigDecimal getCoverage() { return coverage; } + public String getCoverageUrl() { + return coverageUrl; + } + public String getCoverageImageUrl() { return coverageImageUrl; } @@ -132,6 +146,10 @@ public BigDecimal getDuplications() { return duplications; } + public String getDuplicationsUrl() { + return duplicationsUrl; + } + public String getDuplicationsImageUrl() { return duplicationsImageUrl; } @@ -144,6 +162,10 @@ public long getBugCount() { return bugCount; } + public String getBugUrl() { + return bugUrl; + } + public String getBugImageUrl() { return bugImageUrl; } @@ -156,6 +178,10 @@ public long getVulnerabilityCount() { return vulnerabilityCount; } + public String getVulnerabilityUrl() { + return vulnerabilityUrl; + } + public String getVulnerabilityImageUrl() { return vulnerabilityImageUrl; } @@ -164,6 +190,10 @@ public long getCodeSmellCount() { return codeSmellCount; } + public String getCodeSmellUrl() { + return codeSmellUrl; + } + public String getCodeSmellImageUrl() { return codeSmellImageUrl; } @@ -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) @@ -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() { @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java index 36067ab22..c1237efa6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java @@ -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() @@ -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") diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java index 8b5382f43..819da23db 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java @@ -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); + } } -} \ No newline at end of file + @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 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) + ); + } + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java index 552bef1c2..d8bb0d396 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java @@ -48,13 +48,17 @@ void testCreateAnalysisSummary() { .withSummaryImageUrl("summaryImageUrl") .withProjectKey("projectKey") .withBugCount(911) + .withBugUrl("bugUrl") .withBugImageUrl("bugImageUrl") .withCodeSmellCount(1) .withCoverage(BigDecimal.valueOf(303)) + .withCodeSmellUrl("codeSmellUrl") .withCodeSmellImageUrl("codeSmellImageUrl") + .withCoverageUrl("codeCoverageUrl") .withCoverageImageUrl("codeCoverageImageUrl") .withDashboardUrl("dashboardUrl") .withDuplications(BigDecimal.valueOf(66)) + .withDuplicationsUrl("duplicationsUrl") .withDuplicationsImageUrl("duplicationsImageUrl") .withFailedQualityGateConditions(java.util.List.of("issuea", "issueb", "issuec")) .withNewCoverage(BigDecimal.valueOf(99)) @@ -63,6 +67,7 @@ void testCreateAnalysisSummary() { .withStatusImageUrl("statusImageUrl") .withTotalIssueCount(666) .withVulnerabilityCount(96) + .withVulnerabilityUrl("vulnerabilityUrl") .withVulnerabilityImageUrl("vulnerabilityImageUrl") .build(); @@ -85,25 +90,25 @@ void testCreateAnalysisSummary() { new Heading(2, new Text("666 Issues")), new List(List.Style.BULLET, new ListItem( - new Image("Bug","bugImageUrl"), + new Link("bugUrl", new Image("Bug","bugImageUrl")), new Text(" "), new Text("911 Bugs")), new ListItem( - new Image("Vulnerability","vulnerabilityImageUrl"), + new Link("vulnerabilityUrl", new Image("Vulnerability","vulnerabilityImageUrl")), new Text(" "), new Text("165 Vulnerabilities")), new ListItem( - new Image("Code Smell", "codeSmellImageUrl"), + new Link("codeSmellUrl", new Image("Code Smell", "codeSmellImageUrl")), new Text(" "), new Text("1 Code Smell"))), new Heading(2, new Text("Coverage and Duplications")), new List(List.Style.BULLET, new ListItem( - new Image("Coverage", "codeCoverageImageUrl"), + new Link("codeCoverageUrl", new Image("Coverage", "codeCoverageImageUrl")), new Text(" "), new Text("99.00% Coverage (303.00% Estimated after merge)")), new ListItem( - new Image("Duplications", "duplicationsImageUrl"), + new Link("duplicationsUrl", new Image("Duplications", "duplicationsImageUrl")), new Text(" "), new Text("199.00% Duplicated Code (66.00% Estimated after merge)"))), new Paragraph(new Text("**Project ID:** projectKey")), diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java index 2fcaca3c6..3ee31f37a 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java @@ -181,20 +181,25 @@ void shouldProduceCorrectAnalysisSummary(String coverage, String coverageImage, AnalysisSummary expected = AnalysisSummary.builder() .withBugCount(2) + .withBugUrl("http://localhost:9000/project/issues?pullRequest=5&resolved=false&types=BUG&inNewCodePeriod=true&id=projectKey") .withBugImageUrl("http://localhost:9000/static/communityBranchPlugin/common/bug.svg?sanitize=true") .withCoverage(null == coverage ? null : new BigDecimal(coverage)) + .withCoverageUrl("http://localhost:9000/component_measures?id=projectKey&metric=new_coverage&pullRequest=5&view=list") .withCoverageImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/CoverageChart/" + coverageImage) .withNewCoverage(null == coverage ? null : new BigDecimal(coverage)) .withDuplications(null == duplications ? null : new BigDecimal(duplications).setScale(1, RoundingMode.CEILING)) + .withDuplicationsUrl("http://localhost:9000/component_measures?id=projectKey&metric=new_duplicated_lines_density&pullRequest=5&view=list") .withDuplicationsImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/Duplications/" + duplicationsImage) .withNewDuplications(null == duplications ? null : new BigDecimal(duplications)) .withCodeSmellCount(1) + .withCodeSmellUrl("http://localhost:9000/project/issues?pullRequest=5&resolved=false&types=CODE_SMELL&inNewCodePeriod=true&id=projectKey") .withCodeSmellImageUrl("http://localhost:9000/static/communityBranchPlugin/common/code_smell.svg?sanitize=true") .withDashboardUrl("http://localhost:9000/dashboard?id=projectKey&pullRequest=5") .withProjectKey("projectKey") .withSummaryImageUrl("http://localhost:9000/static/communityBranchPlugin/common/icon.png") .withSecurityHotspotCount(1) .withVulnerabilityCount(1) + .withVulnerabilityUrl("http://localhost:9000/project/issues?pullRequest=5&resolved=false&types=VULNERABILITY&inNewCodePeriod=true&id=projectKey") .withVulnerabilityImageUrl("http://localhost:9000/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true") .withStatusDescription("Failed") .withStatusImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true")