]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20552 Refactor external issue format to comply with CCT
authorAlain Kermis <alain.kermis@sonarsource.com>
Thu, 19 Oct 2023 09:35:23 +0000 (11:35 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 20 Oct 2023 20:02:39 +0000 (20:02 +0000)
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReport.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportValidator.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueImporterTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportParserTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportValidatorTest.java
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json [deleted file]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/report.json
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/invalid_report.json [new file with mode: 0644]

index c629025b1de4bd183faefa335068dfafa28b924f..1b08ed881ef04ec307c7980464232276a74723be 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.scanner.externalissue;
 
+import java.util.HashMap;
 import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -58,7 +60,7 @@ public class ExternalIssueImporter {
 
   public void execute() {
     if (report.rules != null) {
-      importRules();
+      importNewFormat();
     } else {
       importDeprecatedFormat();
     }
@@ -74,23 +76,27 @@ public class ExternalIssueImporter {
     logStatistics(issueCount, StringUtils.EMPTY);
   }
 
-  private void importRules() {
+  private void importNewFormat() {
+    Map<String, Rule> rulesMap = new HashMap<>();
+
     for (Rule rule : report.rules) {
+      rulesMap.put(rule.id, rule);
       NewAdHocRule adHocRule = createAdHocRule(rule);
-      int issueCount = 0;
-      for (Issue issue : rule.issues) {
-        if (importIssue(issue, rule)) {
-          issueCount++;
-        }
-      }
-      logStatistics(issueCount, String.format(" for ruleId '%s'", rule.ruleId));
       adHocRule.save();
     }
+
+    int issueCount = 0;
+    for (Issue issue : report.issues) {
+      if (importIssue(issue, rulesMap.get(issue.ruleId))) {
+        issueCount++;
+      }
+    }
+    logStatistics(issueCount, StringUtils.EMPTY);
   }
 
   private NewAdHocRule createAdHocRule(Rule rule) {
     NewAdHocRule adHocRule = context.newAdHocRule();
-    adHocRule.ruleId(rule.ruleId);
+    adHocRule.ruleId(rule.id);
     adHocRule.name(rule.name);
     adHocRule.description(rule.description);
     adHocRule.engineId(rule.engineId);
@@ -151,7 +157,7 @@ public class ExternalIssueImporter {
   private boolean importIssue(Issue issue, ExternalIssueReport.Rule rule) {
     NewExternalIssue externalIssue = context.newExternalIssue()
       .engineId(rule.engineId)
-      .ruleId(rule.ruleId)
+      .ruleId(rule.id)
       .severity(backmapSeverityFromImpact(rule))
       .type(backmapTypeFromImpact(rule));
 
index fcf19637e3799ce0c1d06e107646807a18f00863..e4a52caa55d4f2fe460672764c3c8eebe83f292d 100644 (file)
@@ -42,14 +42,13 @@ public class ExternalIssueReport {
   }
 
   static class Rule {
-    String ruleId;
+    String id;
     String engineId;
     String name;
     @Nullable
     String description;
     String cleanCodeAttribute;
     Impact[] impacts;
-    Issue[] issues;
   }
 
   static class Impact {
index d3b81b9321832077a3a485e0238f9a2a2a84e13b..82541cd2767b48a73d59b1899f1dea62e5639467 100644 (file)
@@ -20,8 +20,9 @@
 package org.sonar.scanner.externalissue;
 
 import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
 import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.scanner.ScannerSide;
@@ -30,7 +31,7 @@ import org.sonar.core.documentation.DocumentationLinkGenerator;
 @ScannerSide
 public class ExternalIssueReportValidator {
   private static final Logger LOGGER = LoggerFactory.getLogger(ExternalIssueReportValidator.class);
-  private static final String RULE_ID = "ruleId";
+  private static final String ISSUE_RULE_ID = "ruleId";
   private static final String SEVERITY = "severity";
   private static final String TYPE = "type";
   private static final String DOCUMENTATION_SUFFIX = "/analyzing-source-code/importing-external-issues/generic-issue-import-format/";
@@ -40,23 +41,42 @@ public class ExternalIssueReportValidator {
     this.documentationLinkGenerator = documentationLinkGenerator;
   }
 
+  /**
+   * <p>Since we are supporting deprecated format, we decide which format it is in order by:
+   *   <ul>
+   *     <li>if both 'rules' and 'issues' fields are present, we assume it is CCT format</li>
+   *     <li>if only 'issues' field is present, we assume it is deprecated format</li>
+   *     <li>otherwise we throw exception as an invalid report was detected</li>
+   *   </ul>
+   * </p>
+   */
   public void validate(ExternalIssueReport report, Path reportPath) {
-    if (report.rules != null) {
-      checkNoField(report.issues, "issues", reportPath);
-      validateRules(report.rules, reportPath);
-    } else if (report.issues != null) {
+    if (report.rules != null && report.issues != null) {
+      Set<String> ruleIds = validateRules(report.rules, reportPath);
+      validateIssuesCctFormat(report.issues, ruleIds, reportPath);
+    } else if (report.rules == null && report.issues != null) {
       String documentationLink = documentationLinkGenerator.getDocumentationLink(DOCUMENTATION_SUFFIX);
       LOGGER.warn("External issues were imported with a deprecated format which will be removed soon. " +
         "Please switch to the newest format to fully benefit from Clean Code: {}", documentationLink);
-      validateIssues(report.issues, reportPath);
+      validateIssuesDeprecatedFormat(report.issues, reportPath);
     } else {
-      throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field 'rules'", reportPath));
+      throw new IllegalStateException(String.format("Failed to parse report '%s': invalid report detected.", reportPath));
     }
   }
 
-  private static void validateIssues(ExternalIssueReport.Issue[] issues, Path reportPath) {
+  private static void validateIssuesCctFormat(ExternalIssueReport.Issue[] issues, Set<String> ruleIds, Path reportPath) {
     for (ExternalIssueReport.Issue issue : issues) {
-      mandatoryField(issue.ruleId, RULE_ID, reportPath);
+      mandatoryField(issue.ruleId, ISSUE_RULE_ID, reportPath);
+      checkRuleExistsInReport(ruleIds, issue, reportPath);
+      checkNoField(issue.severity, SEVERITY, reportPath);
+      checkNoField(issue.type, TYPE, reportPath);
+      validateAlwaysRequiredIssueFields(issue, reportPath);
+    }
+  }
+
+  private static void validateIssuesDeprecatedFormat(ExternalIssueReport.Issue[] issues, Path reportPath) {
+    for (ExternalIssueReport.Issue issue : issues) {
+      mandatoryField(issue.ruleId, ISSUE_RULE_ID, reportPath);
       mandatoryField(issue.severity, SEVERITY, reportPath);
       mandatoryField(issue.type, TYPE, reportPath);
       mandatoryField(issue.engineId, "engineId", reportPath);
@@ -64,24 +84,21 @@ public class ExternalIssueReportValidator {
     }
   }
 
-  private static void validateRules(ExternalIssueReport.Rule[] rules, Path reportPath) {
+  private static Set<String> validateRules(ExternalIssueReport.Rule[] rules, Path reportPath) {
+    Set<String> ruleIds = new HashSet<>();
     for (ExternalIssueReport.Rule rule : rules) {
-      mandatoryField(rule.ruleId, RULE_ID, reportPath);
+      mandatoryField(rule.id, "id", reportPath);
       mandatoryField(rule.name, "name", reportPath);
       mandatoryField(rule.engineId, "engineId", reportPath);
       mandatoryField(rule.cleanCodeAttribute, "cleanCodeAttribute", reportPath);
-      mandatoryArray(rule.impacts, "impacts", reportPath);
-      validateIssuesAsPartOfARule(rule.issues, reportPath);
-    }
-  }
+      checkImpactsArray(rule.impacts, reportPath);
 
-  private static void validateIssuesAsPartOfARule(ExternalIssueReport.Issue[] issues, Path reportPath) {
-    for (ExternalIssueReport.Issue issue : issues) {
-      validateAlwaysRequiredIssueFields(issue, reportPath);
-      checkNoField(issue.severity, SEVERITY, reportPath);
-      checkNoField(issue.type, TYPE, reportPath);
-      checkNoField(issue.ruleId, RULE_ID, reportPath);
+      if (!ruleIds.add(rule.id)) {
+        throw new IllegalStateException(String.format("Failed to parse report '%s': found duplicate rule ID '%s'.", reportPath, rule.id));
+      }
     }
+
+    return ruleIds;
   }
 
   private static void checkNoField(@Nullable Object value, String fieldName, Path reportPath) {
@@ -128,17 +145,17 @@ public class ExternalIssueReportValidator {
     }
   }
 
-  private static void mandatoryArray(@Nullable Object[] value, String fieldName, Path reportPath) {
-    mandatoryField(value, fieldName, reportPath);
+  private static void checkImpactsArray(@Nullable Object[] value, Path reportPath) {
+    mandatoryField(value, "impacts", reportPath);
     if (value.length == 0) {
       throw new IllegalStateException(String.format("Failed to parse report '%s': mandatory array '%s' not populated.", reportPath,
-        fieldName));
+        "impacts"));
     }
   }
 
-  private static void mandatoryField(@Nullable String value, String fieldName, Path reportPath) {
-    if (StringUtils.isBlank(value)) {
-      throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", reportPath, fieldName));
+  private static void checkRuleExistsInReport(Set<String> ruleIds, ExternalIssueReport.Issue issue, Path reportPath) {
+    if (!ruleIds.contains(issue.ruleId)) {
+      throw new IllegalStateException(String.format("Failed to parse report '%s': rule with '%s' not present.", reportPath, issue.ruleId));
     }
   }
 }
index 7b29c33aa0b5e05cd45dfb8ecc8cd23a5a69689d..4641efa95d1f70267a4a48d253fac7b68024526d 100644 (file)
@@ -82,7 +82,7 @@ public class ExternalIssueImporterTest {
   public void execute_whenNewFormatWithZeroIssues() {
     ExternalIssueReport report = new ExternalIssueReport();
     ExternalIssueReport.Rule rule = createRule();
-    rule.issues = new ExternalIssueReport.Issue[0];
+    report.issues = new ExternalIssueReport.Issue[0];
     report.rules = new ExternalIssueReport.Rule[]{rule};
 
     ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report);
@@ -90,7 +90,7 @@ public class ExternalIssueImporterTest {
 
     assertThat(context.allExternalIssues()).isEmpty();
     assertThat(context.allIssues()).isEmpty();
-    assertThat(logs.logs(Level.INFO)).contains("Imported 0 issues in 0 files for ruleId 'some_rule_id'");
+    assertThat(logs.logs(Level.INFO)).contains("Imported 0 issues in 0 files");
   }
 
   @Test
@@ -109,7 +109,7 @@ public class ExternalIssueImporterTest {
     assertThat(output.severity()).isEqualTo(Severity.CRITICAL); //backmapped
     assertThat(output.type()).isEqualTo(RuleType.VULNERABILITY); //backmapped
     assertThat(output.remediationEffort()).isNull();
-    assertThat(logs.logs(Level.INFO)).contains("Imported 1 issue in 1 file for ruleId 'some_rule_id'");
+    assertThat(logs.logs(Level.INFO)).contains("Imported 1 issue in 1 file");
     assertThat(context.allAdHocRules()).hasSize(1);
 
     AdHocRule output1 = context.allAdHocRules().iterator().next();
@@ -177,7 +177,7 @@ public class ExternalIssueImporterTest {
   public void execute_whenNewFormatContainsNonExistentCleanCodeAttribute_shouldThrowException() {
     ExternalIssueReport report = new ExternalIssueReport();
     ExternalIssueReport.Rule rule = createRule("not_existent_attribute", MAINTAINABILITY.name(), HIGH.name());
-    rule.issues = new ExternalIssueReport.Issue[]{};
+    report.issues = new ExternalIssueReport.Issue[]{};
     report.rules = new ExternalIssueReport.Rule[]{rule};
 
     ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report);
@@ -191,7 +191,7 @@ public class ExternalIssueImporterTest {
   public void execute_whenNewFormatContainsNonExistentSoftwareQuality_shouldThrowException() {
     ExternalIssueReport report = new ExternalIssueReport();
     ExternalIssueReport.Rule rule = createRule(CleanCodeAttribute.CONVENTIONAL.name(), "not_existent_software_quality", HIGH.name());
-    rule.issues = new ExternalIssueReport.Issue[]{};
+    report.issues = new ExternalIssueReport.Issue[]{};
     report.rules = new ExternalIssueReport.Rule[]{rule};
 
     ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report);
@@ -206,7 +206,7 @@ public class ExternalIssueImporterTest {
     ExternalIssueReport report = new ExternalIssueReport();
     ExternalIssueReport.Rule rule = createRule(CleanCodeAttribute.CONVENTIONAL.name(), SoftwareQuality.RELIABILITY.name(),
       "not_existent_impact_severity");
-    rule.issues = new ExternalIssueReport.Issue[]{};
+    report.issues = new ExternalIssueReport.Issue[]{};
     report.rules = new ExternalIssueReport.Rule[]{rule};
 
     ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report);
@@ -310,7 +310,7 @@ public class ExternalIssueImporterTest {
 
   private static ExternalIssueReport.Rule createRule(String cleanCodeAttribute, String softwareQuality, String impactSeverity) {
     ExternalIssueReport.Rule rule = new ExternalIssueReport.Rule();
-    rule.ruleId = RULE_ID;
+    rule.id = RULE_ID;
     rule.name = RULE_NAME;
     rule.engineId = RULE_ENGINE_ID;
     rule.cleanCodeAttribute = cleanCodeAttribute;
@@ -342,7 +342,8 @@ public class ExternalIssueImporterTest {
   private void runOn(ExternalIssueReport.Issue input) {
     ExternalIssueReport report = new ExternalIssueReport();
     ExternalIssueReport.Rule rule = createRule();
-    rule.issues = new ExternalIssueReport.Issue[]{input};
+    input.ruleId = rule.id;
+    report.issues = new ExternalIssueReport.Issue[]{input};
     report.rules = new ExternalIssueReport.Rule[]{rule};
 
     ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report);
index 645ecf5d5b3a84d84caaba68cff2ece697b89f86..067dd24304e43a63c907c4ca77dabad55ec7f591 100644 (file)
@@ -121,11 +121,10 @@ public class ExternalIssueReportParserTest {
   }
 
   private static void assertCctReport(ExternalIssueReport report) {
-    assertThat(report.issues).isNull();
     assertThat(report.rules).hasSize(2);
 
     ExternalIssueReport.Rule rule = report.rules[0];
-    assertThat(rule.ruleId).isEqualTo("rule1");
+    assertThat(rule.id).isEqualTo("rule1");
     assertThat(rule.engineId).isEqualTo("test");
     assertThat(rule.name).isEqualTo("just_some_rule_name");
     assertThat(rule.description).isEqualTo("just_some_description");
@@ -136,11 +135,11 @@ public class ExternalIssueReportParserTest {
     assertThat(rule.impacts[1].severity).isEqualTo("LOW");
     assertThat(rule.impacts[1].softwareQuality).isEqualTo("SECURITY");
 
-    assertThat(rule.issues).hasSize(4);
+    assertThat(report.issues).hasSize(8);
 
-    ExternalIssueReport.Issue issue1 = rule.issues[0];
+    ExternalIssueReport.Issue issue1 = report.issues[0];
     assertThat(issue1.engineId).isNull();
-    assertThat(issue1.ruleId).isNull();
+    assertThat(issue1.ruleId).isEqualTo("rule1");
     assertThat(issue1.severity).isNull();
     assertThat(issue1.effortMinutes).isEqualTo(40);
     assertThat(issue1.type).isNull();
@@ -152,9 +151,9 @@ public class ExternalIssueReportParserTest {
     assertThat(issue1.primaryLocation.textRange.endLine).isEqualTo(3);
     assertThat(issue1.secondaryLocations).isNull();
 
-    ExternalIssueReport.Issue issue2 = rule.issues[3];
+    ExternalIssueReport.Issue issue2 = report.issues[7];
     assertThat(issue2.engineId).isNull();
-    assertThat(issue2.ruleId).isNull();
+    assertThat(issue2.ruleId).isEqualTo("rule2");
     assertThat(issue2.severity).isNull();
     assertThat(issue2.effortMinutes).isNull();
     assertThat(issue2.type).isNull();
index 8f7bd5ac173bea845e1370e4729ab945dc316f8d..c466357417a5bcfe19f5a614c4bbdd5f11636a39 100644 (file)
@@ -48,7 +48,7 @@ public class ExternalIssueReportValidatorTest {
   private static final String TEST_URL = "test-url";
 
   private final Gson gson = new Gson();
-  private final Path reportPath = Paths.get("some-folder");
+  private final Path reportPath = Paths.get("report-path");
   private final DocumentationLinkGenerator documentationLinkGenerator = mock(DocumentationLinkGenerator.class);
   private final ExternalIssueReportValidator validator = new ExternalIssueReportValidator(documentationLinkGenerator);
 
@@ -60,12 +60,30 @@ public class ExternalIssueReportValidatorTest {
     when(documentationLinkGenerator.getDocumentationLink(URL)).thenReturn(TEST_URL);
   }
 
+  @Test
+  public void validate_whenInvalidReport_shouldThrowException() throws IOException {
+    ExternalIssueReport report = readInvalidReport(DEPRECATED_REPORTS_LOCATION);
+    assertThatThrownBy(() -> validator.validate(report, reportPath))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Failed to parse report 'report-path': invalid report detected.");
+  }
+
   @Test
   public void validate_whenCorrect_shouldNotThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
     assertThatCode(() -> validator.validate(report, reportPath)).doesNotThrowAnyException();
   }
 
+  @Test
+  public void validate_whenDuplicateRuleIdFound_shouldThrowException() throws IOException {
+    ExternalIssueReport report = read(REPORTS_LOCATION);
+    report.rules[0].id = "rule2";
+
+    assertThatThrownBy(() -> validator.validate(report, reportPath))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Failed to parse report 'report-path': found duplicate rule ID 'rule2'.");
+  }
+
   @Test
   public void validate_whenMissingMandatoryCleanCodeAttributeField_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
@@ -73,7 +91,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'cleanCodeAttribute'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'cleanCodeAttribute'.");
   }
 
   @Test
@@ -83,17 +101,17 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'engineId'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'engineId'.");
   }
 
   @Test
   public void validate_whenMissingFilepathFieldForPrimaryLocation_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[0].primaryLocation.filePath = null;
+    report.issues[0].primaryLocation.filePath = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in the primary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in the primary location of the issue.");
   }
 
   @Test
@@ -103,57 +121,57 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'impacts'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'impacts'.");
   }
 
   @Test
   public void validate_whenMissingMessageFieldForPrimaryLocation_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[0].primaryLocation.message = null;
+    report.issues[0].primaryLocation.message = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'message' in the primary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'message' in the primary location of the issue.");
   }
 
   @Test
   public void validate_whenMissingStartLineFieldForPrimaryLocation_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[0].primaryLocation.textRange.startLine = null;
+    report.issues[0].primaryLocation.textRange.startLine = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in the primary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in the primary location of the issue.");
   }
 
   @Test
   public void validate_whenReportMissingFilePathForSecondaryLocation_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[3].secondaryLocations[0].filePath = null;
+    report.issues[3].secondaryLocations[0].filePath = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in a secondary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in a secondary location of the issue.");
   }
 
   @Test
   public void validate_whenReportMissingTextRangeForSecondaryLocation_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[3].secondaryLocations[0].textRange = null;
+    report.issues[3].secondaryLocations[0].textRange = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'textRange' in a secondary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'textRange' in a secondary location of the issue.");
   }
 
   @Test
   public void validate_whenReportMissingTextRangeStartLineForSecondaryLocation_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[3].secondaryLocations[0].textRange.startLine = null;
+    report.issues[3].secondaryLocations[0].textRange.startLine = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in a secondary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in a secondary location of the issue.");
   }
 
   @Test
@@ -163,57 +181,73 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'name'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'name'.");
   }
 
   @Test
   public void validate_whenMissingPrimaryLocationField_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[0].primaryLocation = null;
+    report.issues[0].primaryLocation = null;
+
+    assertThatThrownBy(() -> validator.validate(report, reportPath))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'primaryLocation'.");
+  }
+
+  @Test
+  public void validate_whenMissingOrEmptyRuleIdField_shouldThrowException() throws IOException {
+    String errorMessage = "Failed to parse report 'report-path': missing mandatory field 'id'.";
+
+    ExternalIssueReport report = read(REPORTS_LOCATION);
+    report.rules[0].id = null;
+    assertThatThrownBy(() -> validator.validate(report, reportPath))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage(errorMessage);
 
+    report.rules[0].id = "";
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'primaryLocation'.");
+      .hasMessage(errorMessage);
   }
 
   @Test
-  public void validate_whenMissingRuleIdField_shouldThrowException() throws IOException {
+  public void validate_whenIssueContainsRuleIdNotPresentInReport_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].ruleId = null;
+    report.issues[0].ruleId = "rule-id-not-present";
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'ruleId'.");
+      .hasMessage("Failed to parse report 'report-path': rule with 'rule-id-not-present' not present.");
   }
 
   @Test
-  public void validate_whenContainsDeprecatedIssuesEntry_shouldThrowException() throws IOException {
+  public void validate_whenIssueRuleIdNotPresentInReport_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.issues = new ExternalIssueReport.Issue[] { new ExternalIssueReport.Issue() };
+    report.issues[0].ruleId = null;
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Deprecated 'issues' field found in the following report: 'some-folder'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'ruleId'.");
   }
 
   @Test
   public void validate_whenContainsDeprecatedSeverityEntry_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[0].severity = "MAJOR";
+    report.issues[0].severity = "MAJOR";
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Deprecated 'severity' field found in the following report: 'some-folder'.");
+      .hasMessage("Deprecated 'severity' field found in the following report: 'report-path'.");
   }
 
   @Test
   public void validate_whenContainsDeprecatedTypeEntry_shouldThrowException() throws IOException {
     ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].issues[0].type = "BUG";
+    report.issues[0].type = "BUG";
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Deprecated 'type' field found in the following report: 'some-folder'.");
+      .hasMessage("Deprecated 'type' field found in the following report: 'report-path'.");
   }
 
   @Test
@@ -223,7 +257,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': mandatory array 'impacts' not populated.");
+      .hasMessage("Failed to parse report 'report-path': mandatory array 'impacts' not populated.");
   }
 
   @Test
@@ -240,7 +274,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'engineId'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'engineId'.");
     assertWarningLog();
   }
 
@@ -251,7 +285,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in the primary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in the primary location of the issue.");
     assertWarningLog();
   }
 
@@ -262,7 +296,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'message' in the primary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'message' in the primary location of the issue.");
     assertWarningLog();
   }
 
@@ -273,7 +307,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'primaryLocation'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'primaryLocation'.");
     assertWarningLog();
   }
 
@@ -284,7 +318,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in the primary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in the primary location of the issue.");
     assertWarningLog();
   }
 
@@ -295,7 +329,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in a secondary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in a secondary location of the issue.");
     assertWarningLog();
   }
 
@@ -306,7 +340,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'textRange' in a secondary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'textRange' in a secondary location of the issue.");
     assertWarningLog();
   }
 
@@ -317,7 +351,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in a secondary location of the issue.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in a secondary location of the issue.");
     assertWarningLog();
   }
 
@@ -328,7 +362,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'ruleId'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'ruleId'.");
     assertWarningLog();
   }
 
@@ -339,7 +373,7 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'severity'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'severity'.");
     assertWarningLog();
   }
 
@@ -350,20 +384,10 @@ public class ExternalIssueReportValidatorTest {
 
     assertThatThrownBy(() -> validator.validate(report, reportPath))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'type'.");
+      .hasMessage("Failed to parse report 'report-path': missing mandatory field 'type'.");
     assertWarningLog();
   }
 
-  @Test
-  public void validate_whenEmptyStringForMandatoryField_shouldStillThrowException() throws IOException {
-    ExternalIssueReport report = read(REPORTS_LOCATION);
-    report.rules[0].ruleId = "";
-
-    assertThatThrownBy(() -> validator.validate(report, reportPath))
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'ruleId'.");
-  }
-
   private void assertWarningLog() {
     assertThat(logTester.getLogs(Level.WARN))
       .extracting(LogAndArguments::getFormattedMsg)
@@ -376,4 +400,9 @@ public class ExternalIssueReportValidatorTest {
     return gson.fromJson(reader, ExternalIssueReport.class);
   }
 
+  private ExternalIssueReport readInvalidReport(String location) throws IOException {
+    Reader reader = Files.newBufferedReader(Paths.get(location + "invalid_report.json"), StandardCharsets.UTF_8);
+    return gson.fromJson(reader, ExternalIssueReport.class);
+  }
+
 }
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json
deleted file mode 100644 (file)
index 6e858fd..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "some_other_field": "with_some_values"
-}
index 7f797d663b6f1803b95b1bd61752c30a29662e2e..878432e24b4d6317f0dea1a43b1a01fccdb596a9 100644 (file)
@@ -1,7 +1,7 @@
 {
   "rules": [
     {
-      "ruleId": "rule1",
+      "id": "rule1",
       "name": "just_some_rule_name",
       "description": "just_some_description",
       "engineId": "test",
           "softwareQuality": "SECURITY",
           "severity": "LOW"
         }
-      ],
-      "issues": [
-        {
-          "effortMinutes": 40,
-          "primaryLocation": {
-            "message": "fix the issue here",
-            "filePath": "file1.js",
-            "textRange": {
-              "startLine": 1,
-              "startColumn": 2,
-              "endLine": 3,
-              "endColumn": 4
-            }
-          }
-        },
-        {
-          "primaryLocation": {
-            "message": "fix the bug here",
-            "filePath": "file2.js",
-            "textRange": {
-              "startLine": 3
-            }
-          }
-        },
-        {
-          "primaryLocation": {
-            "message": "fix the bug here",
-            "filePath": "file3.js"
-          }
-        },
-        {
-          "primaryLocation": {
-            "message": "fix the bug here",
-            "filePath": "file3.js"
-          },
-          "secondaryLocations": [
-            {
-              "message": "fix the bug here",
-              "filePath": "file1.js",
-              "textRange": {
-                "startLine": 1
-              }
-            },
-            {
-              "filePath": "file2.js",
-              "textRange": {
-                "startLine": 2
-              }
-            }
-          ]
-        }
       ]
     },
     {
-      "ruleId": "rule2",
+      "id": "rule2",
       "name": "just_some_other_rule_name",
       "description": "just_some_description",
       "engineId": "test2",
           "softwareQuality": "RELIABILITY",
           "severity": "LOW"
         }
-      ],
-      "issues": [
+      ]
+    }
+  ],
+  "issues": [
+    {
+      "ruleId": "rule1",
+      "effortMinutes": 40,
+      "primaryLocation": {
+        "message": "fix the issue here",
+        "filePath": "file1.js",
+        "textRange": {
+          "startLine": 1,
+          "startColumn": 2,
+          "endLine": 3,
+          "endColumn": 4
+        }
+      }
+    },
+    {
+      "ruleId": "rule1",
+      "primaryLocation": {
+        "message": "fix the bug here",
+        "filePath": "file2.js",
+        "textRange": {
+          "startLine": 3
+        }
+      }
+    },
+    {
+      "ruleId": "rule1",
+      "primaryLocation": {
+        "message": "fix the bug here",
+        "filePath": "file3.js"
+      }
+    },
+    {
+      "ruleId": "rule1",
+      "primaryLocation": {
+        "message": "fix the bug here",
+        "filePath": "file3.js"
+      },
+      "secondaryLocations": [
         {
-          "effortMinutes": 40,
-          "primaryLocation": {
-            "message": "fix the issue here",
-            "filePath": "file1.js",
-            "textRange": {
-              "startLine": 1,
-              "startColumn": 2,
-              "endLine": 3,
-              "endColumn": 4
-            }
+          "message": "fix the bug here",
+          "filePath": "file1.js",
+          "textRange": {
+            "startLine": 1
           }
         },
         {
-          "primaryLocation": {
-            "message": "fix the bug here",
-            "filePath": "file2.js",
-            "textRange": {
-              "startLine": 3
-            }
+          "filePath": "file2.js",
+          "textRange": {
+            "startLine": 2
           }
-        },
+        }
+      ]
+    },
+    {
+      "ruleId": "rule2",
+      "effortMinutes": 40,
+      "primaryLocation": {
+        "message": "fix the issue here",
+        "filePath": "file1.js",
+        "textRange": {
+          "startLine": 1,
+          "startColumn": 2,
+          "endLine": 3,
+          "endColumn": 4
+        }
+      }
+    },
+    {
+      "ruleId": "rule2",
+      "primaryLocation": {
+        "message": "fix the bug here",
+        "filePath": "file2.js",
+        "textRange": {
+          "startLine": 3
+        }
+      }
+    },
+    {
+      "ruleId": "rule2",
+      "primaryLocation": {
+        "message": "fix the bug here",
+        "filePath": "file3.js"
+      }
+    },
+    {
+      "ruleId": "rule2",
+      "primaryLocation": {
+        "message": "fix the bug here",
+        "filePath": "file3.js"
+      },
+      "secondaryLocations": [
         {
-          "primaryLocation": {
-            "message": "fix the bug here",
-            "filePath": "file3.js"
+          "message": "fix the bug here",
+          "filePath": "file1.js",
+          "textRange": {
+            "startLine": 1
           }
         },
         {
-          "primaryLocation": {
-            "message": "fix the bug here",
-            "filePath": "file3.js"
-          },
-          "secondaryLocations": [
-            {
-              "message": "fix the bug here",
-              "filePath": "file1.js",
-              "textRange": {
-                "startLine": 1
-              }
-            },
-            {
-              "filePath": "file2.js",
-              "textRange": {
-                "startLine": 2
-              }
-            }
-          ]
+          "filePath": "file2.js",
+          "textRange": {
+            "startLine": 2
+          }
         }
       ]
     }
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/invalid_report.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/invalid_report.json
new file mode 100644 (file)
index 0000000..6e858fd
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "some_other_field": "with_some_values"
+}