3 * Copyright (C) 2009-2022 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.ce.task.projectanalysis.issue;
22 import com.google.common.collect.Iterators;
23 import com.tngtech.java.junit.dataprovider.DataProvider;
24 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
25 import com.tngtech.java.junit.dataprovider.UseDataProvider;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Iterator;
29 import java.util.stream.IntStream;
30 import org.apache.commons.codec.digest.DigestUtils;
31 import org.junit.Before;
32 import org.junit.Rule;
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 import org.sonar.api.rule.RuleKey;
36 import org.sonar.api.rule.Severity;
37 import org.sonar.api.rules.RuleType;
38 import org.sonar.api.utils.Duration;
39 import org.sonar.ce.task.projectanalysis.batch.BatchReportReaderRule;
40 import org.sonar.ce.task.projectanalysis.component.Component;
41 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
42 import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
43 import org.sonar.ce.task.projectanalysis.issue.commonrule.CommonRuleEngine;
44 import org.sonar.ce.task.projectanalysis.issue.filter.IssueFilter;
45 import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule;
46 import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderRule;
47 import org.sonar.ce.task.projectanalysis.source.SourceLinesHashRepository;
48 import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository;
49 import org.sonar.core.issue.DefaultIssue;
50 import org.sonar.core.issue.tracking.Input;
51 import org.sonar.core.util.CloseableIterator;
52 import org.sonar.db.protobuf.DbIssues;
53 import org.sonar.scanner.protocol.Constants;
54 import org.sonar.scanner.protocol.output.ScannerReport;
55 import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
56 import org.sonar.scanner.protocol.output.ScannerReport.TextRange;
57 import org.sonar.server.rule.CommonRuleKeys;
59 import static java.util.Arrays.asList;
60 import static java.util.Collections.emptyMap;
61 import static java.util.Collections.singletonList;
62 import static org.assertj.core.api.Assertions.assertThat;
63 import static org.mockito.ArgumentMatchers.any;
64 import static org.mockito.ArgumentMatchers.eq;
65 import static org.mockito.Mockito.mock;
66 import static org.mockito.Mockito.when;
67 import static org.sonar.api.issue.Issue.STATUS_OPEN;
68 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
70 @RunWith(DataProviderRunner.class)
71 public class TrackerRawInputFactoryTest {
73 private static final String FILE_UUID = "fake_uuid";
74 private static final String ANOTHER_FILE_UUID = "another_fake_uuid";
75 private static final String EXAMPLE_LINE_OF_CODE_FORMAT = "int example = line + of + code + %d; ";
76 private static final int FILE_REF = 2;
77 private static final int NOT_IN_REPORT_FILE_REF = 3;
78 private static final int ANOTHER_FILE_REF = 4;
81 public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule().setRoot(PROJECT);
83 public BatchReportReaderRule reportReader = new BatchReportReaderRule();
85 public ActiveRulesHolderRule activeRulesHolder = new ActiveRulesHolderRule();
87 public RuleRepositoryRule ruleRepository = new RuleRepositoryRule();
89 private static final ReportComponent FILE = ReportComponent.builder(Component.Type.FILE, FILE_REF).setUuid(FILE_UUID).build();
90 private static final ReportComponent ANOTHER_FILE = ReportComponent.builder(Component.Type.FILE, ANOTHER_FILE_REF).setUuid(ANOTHER_FILE_UUID).build();
91 private static final ReportComponent PROJECT = ReportComponent.builder(Component.Type.PROJECT, 1).addChildren(FILE, ANOTHER_FILE).build();
93 private final SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class);
94 private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class);
95 private final CommonRuleEngine commonRuleEngine = mock(CommonRuleEngine.class);
96 private final IssueFilter issueFilter = mock(IssueFilter.class);
97 private final TrackerRawInputFactory underTest = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash,
98 sourceLinesRepository, commonRuleEngine, issueFilter, ruleRepository, activeRulesHolder);
101 public void before() {
102 Iterator<String> stringIterator = IntStream.rangeClosed(1, 9)
103 .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i))
105 when(sourceLinesRepository.readLines(any())).thenReturn(CloseableIterator.from(stringIterator));
109 public void load_source_hash_sequences() {
110 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
111 Input<DefaultIssue> input = underTest.create(FILE);
113 assertThat(input.getLineHashSequence()).isNotNull();
114 assertThat(input.getLineHashSequence().getHashForLine(1)).isEqualTo("line");
115 assertThat(input.getLineHashSequence().getHashForLine(2)).isEmpty();
116 assertThat(input.getLineHashSequence().getHashForLine(3)).isEmpty();
118 assertThat(input.getBlockHashSequence()).isNotNull();
122 public void load_source_hash_sequences_only_on_files() {
123 Input<DefaultIssue> input = underTest.create(PROJECT);
125 assertThat(input.getLineHashSequence()).isNotNull();
126 assertThat(input.getBlockHashSequence()).isNotNull();
130 public void load_issues_from_report() {
131 RuleKey ruleKey = RuleKey.of("java", "S001");
132 markRuleAsActive(ruleKey);
133 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
135 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
136 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
137 .setTextRange(newTextRange(2))
138 .setMsg("the message")
139 .setRuleRepository(ruleKey.repository())
140 .setRuleKey(ruleKey.rule())
141 .setSeverity(Constants.Severity.BLOCKER)
143 .setQuickFixAvailable(true)
145 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
146 Input<DefaultIssue> input = underTest.create(FILE);
148 Collection<DefaultIssue> issues = input.getIssues();
149 assertThat(issues).hasSize(1);
150 DefaultIssue issue = Iterators.getOnlyElement(issues.iterator());
152 // fields set by analysis report
153 assertThat(issue.ruleKey()).isEqualTo(ruleKey);
154 assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
155 assertThat(issue.line()).isEqualTo(2);
156 assertThat(issue.gap()).isEqualTo(3.14);
157 assertThat(issue.message()).isEqualTo("the message");
158 assertThat(issue.isQuickFixAvailable()).isTrue();
160 // fields set by compute engine
161 assertThat(issue.checksum()).isEqualTo(input.getLineHashSequence().getHashForLine(2));
162 assertThat(issue.tags()).isEmpty();
163 assertInitializedIssue(issue);
164 assertThat(issue.effort()).isNull();
166 assertLocationHashIsMadeOf(input, "intexample=line+of+code+2;");
170 public void calculateLocationHash_givenIssueOn3Lines_calculateHashOn3Lines() {
171 RuleKey ruleKey = RuleKey.of("java", "S001");
172 markRuleAsActive(ruleKey);
173 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
175 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
176 .setTextRange(TextRange.newBuilder()
180 .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
182 .setMsg("the message")
183 .setRuleRepository(ruleKey.repository())
184 .setRuleKey(ruleKey.rule())
186 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
188 Input<DefaultIssue> input = underTest.create(FILE);
190 assertLocationHashIsMadeOf(input, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
194 public void calculateLocationHash_givenIssuePartiallyOn1Line_calculateHashOnAPartOfLine() {
195 RuleKey ruleKey = RuleKey.of("java", "S001");
196 markRuleAsActive(ruleKey);
197 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
199 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
200 .setTextRange(TextRange.newBuilder()
204 .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
206 .setMsg("the message")
207 .setRuleRepository(ruleKey.repository())
208 .setRuleKey(ruleKey.rule())
210 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
212 Input<DefaultIssue> input = underTest.create(FILE);
214 assertLocationHashIsMadeOf(input, "line+of+code+1;");
218 public void calculateLocationHash_givenIssuePartiallyOn1LineAndPartiallyOnThirdLine_calculateHashAccordingly() {
219 RuleKey ruleKey = RuleKey.of("java", "S001");
220 markRuleAsActive(ruleKey);
221 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
223 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
224 .setTextRange(TextRange.newBuilder()
230 .setMsg("the message")
231 .setRuleRepository(ruleKey.repository())
232 .setRuleKey(ruleKey.rule())
234 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
236 Input<DefaultIssue> input = underTest.create(FILE);
238 assertLocationHashIsMadeOf(input, "line+of+code+1;intexample=line+of+code+2;intexample");
242 public void set_rule_name_as_message_when_issue_message_from_report_is_empty() {
243 RuleKey ruleKey = RuleKey.of("java", "S001");
244 markRuleAsActive(ruleKey);
245 registerRule(ruleKey, "Rule 1");
246 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
247 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
248 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
249 .setRuleRepository(ruleKey.repository())
250 .setRuleKey(ruleKey.rule())
253 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
254 Input<DefaultIssue> input = underTest.create(FILE);
256 Collection<DefaultIssue> issues = input.getIssues();
257 assertThat(issues).hasSize(1);
258 DefaultIssue issue = Iterators.getOnlyElement(issues.iterator());
260 // fields set by analysis report
261 assertThat(issue.ruleKey()).isEqualTo(ruleKey);
263 // fields set by compute engine
264 assertInitializedIssue(issue);
265 assertThat(issue.message()).isEqualTo("Rule 1");
270 public void load_issues_from_report_missing_secondary_location_component() {
271 RuleKey ruleKey = RuleKey.of("java", "S001");
272 markRuleAsActive(ruleKey);
273 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
275 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
276 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
277 .setTextRange(newTextRange(2))
278 .setMsg("the message")
279 .setRuleRepository(ruleKey.repository())
280 .setRuleKey(ruleKey.rule())
281 .setSeverity(Constants.Severity.BLOCKER)
283 .addFlow(ScannerReport.Flow.newBuilder()
284 .addLocation(ScannerReport.IssueLocation.newBuilder()
285 .setComponentRef(FILE_REF)
286 .setMsg("Secondary location in same file")
287 .setTextRange(newTextRange(2)))
288 .addLocation(ScannerReport.IssueLocation.newBuilder()
289 .setComponentRef(NOT_IN_REPORT_FILE_REF)
290 .setMsg("Secondary location in a missing file")
291 .setTextRange(newTextRange(3)))
292 .addLocation(ScannerReport.IssueLocation.newBuilder()
293 .setComponentRef(ANOTHER_FILE_REF)
294 .setMsg("Secondary location in another file")
295 .setTextRange(newTextRange(3)))
298 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
299 Input<DefaultIssue> input = underTest.create(FILE);
301 Collection<DefaultIssue> issues = input.getIssues();
302 assertThat(issues).hasSize(1);
303 DefaultIssue issue = Iterators.getOnlyElement(issues.iterator());
305 DbIssues.Locations locations = issue.getLocations();
306 // fields set by analysis report
307 assertThat(locations.getFlowList()).hasSize(1);
308 assertThat(locations.getFlow(0).getLocationList()).hasSize(2);
309 // Not component id if location is in the same file
310 assertThat(locations.getFlow(0).getLocation(0).getComponentId()).isEmpty();
311 assertThat(locations.getFlow(0).getLocation(1).getComponentId()).isEqualTo(ANOTHER_FILE_UUID);
315 @UseDataProvider("ruleTypeAndStatusByIssueType")
316 public void load_external_issues_from_report(IssueType issueType, RuleType expectedRuleType, String expectedStatus) {
317 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
318 ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder()
319 .setTextRange(newTextRange(2))
320 .setMsg("the message")
321 .setEngineId("eslint")
323 .setSeverity(Constants.Severity.BLOCKER)
327 reportReader.putExternalIssues(FILE.getReportAttributes().getRef(), asList(reportIssue));
328 Input<DefaultIssue> input = underTest.create(FILE);
330 Collection<DefaultIssue> issues = input.getIssues();
331 assertThat(issues).hasSize(1);
332 DefaultIssue issue = Iterators.getOnlyElement(issues.iterator());
334 // fields set by analysis report
335 assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("external_eslint", "S001"));
336 assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
337 assertThat(issue.line()).isEqualTo(2);
338 assertThat(issue.effort()).isEqualTo(Duration.create(20L));
339 assertThat(issue.message()).isEqualTo("the message");
340 assertThat(issue.type()).isEqualTo(expectedRuleType);
342 // fields set by compute engine
343 assertThat(issue.checksum()).isEqualTo(input.getLineHashSequence().getHashForLine(2));
344 assertThat(issue.tags()).isEmpty();
345 assertInitializedExternalIssue(issue, expectedStatus);
349 public static Object[][] ruleTypeAndStatusByIssueType() {
350 return new Object[][] {
351 {IssueType.CODE_SMELL, RuleType.CODE_SMELL, STATUS_OPEN},
352 {IssueType.BUG, RuleType.BUG, STATUS_OPEN},
353 {IssueType.VULNERABILITY, RuleType.VULNERABILITY, STATUS_OPEN},
354 {IssueType.SECURITY_HOTSPOT, RuleType.SECURITY_HOTSPOT, STATUS_TO_REVIEW}
359 @UseDataProvider("ruleTypeAndStatusByIssueType")
360 public void load_external_issues_from_report_with_default_effort(IssueType issueType, RuleType expectedRuleType, String expectedStatus) {
361 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
362 ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder()
363 .setTextRange(newTextRange(2))
364 .setMsg("the message")
365 .setEngineId("eslint")
367 .setSeverity(Constants.Severity.BLOCKER)
370 reportReader.putExternalIssues(FILE.getReportAttributes().getRef(), asList(reportIssue));
371 Input<DefaultIssue> input = underTest.create(FILE);
373 Collection<DefaultIssue> issues = input.getIssues();
374 assertThat(issues).hasSize(1);
375 DefaultIssue issue = Iterators.getOnlyElement(issues.iterator());
377 // fields set by analysis report
378 assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("external_eslint", "S001"));
379 assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
380 assertThat(issue.line()).isEqualTo(2);
381 assertThat(issue.effort()).isEqualTo(Duration.create(0L));
382 assertThat(issue.message()).isEqualTo("the message");
383 assertThat(issue.type()).isEqualTo(expectedRuleType);
385 // fields set by compute engine
386 assertThat(issue.checksum()).isEqualTo(input.getLineHashSequence().getHashForLine(2));
387 assertThat(issue.tags()).isEmpty();
388 assertInitializedExternalIssue(issue, expectedStatus);
392 public void excludes_issues_on_inactive_rules() {
393 RuleKey ruleKey = RuleKey.of("java", "S001");
394 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
396 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
397 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
398 .setTextRange(newTextRange(2))
399 .setMsg("the message")
400 .setRuleRepository(ruleKey.repository())
401 .setRuleKey(ruleKey.rule())
402 .setSeverity(Constants.Severity.BLOCKER)
405 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
406 Input<DefaultIssue> input = underTest.create(FILE);
408 Collection<DefaultIssue> issues = input.getIssues();
409 assertThat(issues).isEmpty();
413 public void filter_excludes_issues_from_report() {
414 RuleKey ruleKey = RuleKey.of("java", "S001");
415 markRuleAsActive(ruleKey);
416 when(issueFilter.accept(any(), eq(FILE))).thenReturn(false);
417 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
418 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
419 .setTextRange(newTextRange(2))
420 .setMsg("the message")
421 .setRuleRepository(ruleKey.repository())
422 .setRuleKey(ruleKey.rule())
423 .setSeverity(Constants.Severity.BLOCKER)
426 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
427 Input<DefaultIssue> input = underTest.create(FILE);
429 Collection<DefaultIssue> issues = input.getIssues();
430 assertThat(issues).isEmpty();
434 public void exclude_issues_on_common_rules() {
435 RuleKey ruleKey = RuleKey.of(CommonRuleKeys.commonRepositoryForLang("java"), "S001");
436 markRuleAsActive(ruleKey);
437 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
438 ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
439 .setMsg("the message")
440 .setRuleRepository(ruleKey.repository())
441 .setRuleKey(ruleKey.rule())
442 .setSeverity(Constants.Severity.BLOCKER)
444 reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
446 Input<DefaultIssue> input = underTest.create(FILE);
448 assertThat(input.getIssues()).isEmpty();
452 public void load_issues_of_compute_engine_common_rules() {
453 RuleKey ruleKey = RuleKey.of(CommonRuleKeys.commonRepositoryForLang("java"), "InsufficientCoverage");
454 markRuleAsActive(ruleKey);
455 when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
456 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
457 DefaultIssue ceIssue = new DefaultIssue()
459 .setMessage("not enough coverage")
461 when(commonRuleEngine.process(FILE)).thenReturn(singletonList(ceIssue));
463 Input<DefaultIssue> input = underTest.create(FILE);
465 assertThat(input.getIssues()).containsOnly(ceIssue);
466 assertInitializedIssue(input.getIssues().iterator().next());
470 public void filter_exclude_issues_on_common_rule() {
471 RuleKey ruleKey = RuleKey.of(CommonRuleKeys.commonRepositoryForLang("java"), "InsufficientCoverage");
472 markRuleAsActive(ruleKey);
473 when(issueFilter.accept(any(), eq(FILE))).thenReturn(false);
474 when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line"));
475 DefaultIssue ceIssue = new DefaultIssue()
477 .setMessage("not enough coverage")
479 when(commonRuleEngine.process(FILE)).thenReturn(singletonList(ceIssue));
481 Input<DefaultIssue> input = underTest.create(FILE);
483 assertThat(input.getIssues()).isEmpty();
486 private void assertInitializedIssue(DefaultIssue issue) {
487 assertInitializedExternalIssue(issue, STATUS_OPEN);
488 assertThat(issue.effort()).isNull();
489 assertThat(issue.effortInMinutes()).isNull();
492 private void assertInitializedExternalIssue(DefaultIssue issue, String expectedStatus) {
493 assertThat(issue.projectKey()).isEqualTo(PROJECT.getKey());
494 assertThat(issue.componentKey()).isEqualTo(FILE.getKey());
495 assertThat(issue.componentUuid()).isEqualTo(FILE.getUuid());
496 assertThat(issue.resolution()).isNull();
497 assertThat(issue.status()).isEqualTo(expectedStatus);
498 assertThat(issue.key()).isNull();
499 assertThat(issue.authorLogin()).isNull();
502 private void markRuleAsActive(RuleKey ruleKey) {
503 activeRulesHolder.put(new ActiveRule(ruleKey, Severity.CRITICAL, emptyMap(), 1_000L, null, "qp1"));
506 private void registerRule(RuleKey ruleKey, String name) {
507 DumbRule dumbRule = new DumbRule(ruleKey);
508 dumbRule.setName(name);
509 ruleRepository.add(dumbRule);
512 private TextRange newTextRange(int issueOnLine) {
513 return TextRange.newBuilder()
514 .setStartLine(issueOnLine)
515 .setEndLine(issueOnLine)
517 .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
521 private void assertLocationHashIsMadeOf(Input<DefaultIssue> input, String stringToHash) {
522 DefaultIssue defaultIssue = Iterators.getOnlyElement(input.getIssues().iterator());
523 String expectedHash = DigestUtils.md5Hex(stringToHash);
524 DbIssues.Locations locations = defaultIssue.getLocations();
526 assertThat(locations.getChecksum()).isEqualTo(expectedHash);