3 * Copyright (C) 2009-2024 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.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.util.Arrays;
26 import java.util.Date;
27 import java.util.HashMap;
29 import java.util.Optional;
30 import java.util.function.BiConsumer;
31 import java.util.stream.Stream;
32 import org.apache.commons.lang3.ArrayUtils;
33 import org.junit.Before;
34 import org.junit.Test;
35 import org.junit.runner.RunWith;
36 import org.sonar.api.rule.RuleKey;
37 import org.sonar.ce.task.projectanalysis.analysis.Analysis;
38 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
39 import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin;
40 import org.sonar.ce.task.projectanalysis.component.Component;
41 import org.sonar.ce.task.projectanalysis.filemove.AddedFileRepository;
42 import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule;
43 import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder;
44 import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository;
45 import org.sonar.ce.task.projectanalysis.scm.Changeset;
46 import org.sonar.ce.task.projectanalysis.scm.ScmInfo;
47 import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository;
48 import org.sonar.core.issue.DefaultIssue;
49 import org.sonar.db.protobuf.DbCommons.TextRange;
50 import org.sonar.db.protobuf.DbIssues;
51 import org.sonar.db.protobuf.DbIssues.Flow;
52 import org.sonar.db.protobuf.DbIssues.Location;
53 import org.sonar.db.protobuf.DbIssues.Locations.Builder;
54 import org.sonar.server.issue.IssueFieldsSetter;
56 import static org.assertj.core.api.Assertions.assertThatThrownBy;
57 import static org.mockito.ArgumentMatchers.any;
58 import static org.mockito.ArgumentMatchers.eq;
59 import static org.mockito.ArgumentMatchers.same;
60 import static org.mockito.Mockito.atLeastOnce;
61 import static org.mockito.Mockito.mock;
62 import static org.mockito.Mockito.never;
63 import static org.mockito.Mockito.verify;
64 import static org.mockito.Mockito.verifyNoInteractions;
65 import static org.mockito.Mockito.when;
66 import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UNCHANGED;
68 @RunWith(DataProviderRunner.class)
69 public class IssueCreationDateCalculatorTest {
70 private static final String COMPONENT_UUID = "ab12";
73 public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
75 private ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class);
76 private IssueFieldsSetter issueUpdater = mock(IssueFieldsSetter.class);
77 private ActiveRulesHolder activeRulesHolder = mock(ActiveRulesHolder.class);
78 private Component component = mock(Component.class);
79 private RuleKey ruleKey = RuleKey.of("reop", "rule");
80 private DefaultIssue issue = mock(DefaultIssue.class);
81 private ActiveRule activeRule = mock(ActiveRule.class);
83 private IssueCreationDateCalculator underTest;
85 private Analysis baseAnalysis = mock(Analysis.class);
86 private Map<String, ScannerPlugin> scannerPlugins = new HashMap<>();
87 private RuleRepository ruleRepository = mock(RuleRepository.class);
88 private AddedFileRepository addedFileRepository = mock(AddedFileRepository.class);
89 private QProfileStatusRepository qProfileStatusRepository = mock(QProfileStatusRepository.class);
90 private ScmInfo scmInfo;
91 private Rule rule = mock(Rule.class);
94 public void before() {
95 analysisMetadataHolder.setScannerPluginsByKey(scannerPlugins);
96 analysisMetadataHolder.setAnalysisDate(new Date());
97 when(component.getUuid()).thenReturn(COMPONENT_UUID);
98 underTest = new IssueCreationDateCalculator(analysisMetadataHolder, scmInfoRepository, issueUpdater, activeRulesHolder, ruleRepository, addedFileRepository, qProfileStatusRepository);
100 when(ruleRepository.findByKey(ruleKey)).thenReturn(Optional.of(rule));
101 when(activeRulesHolder.get(any(RuleKey.class))).thenReturn(Optional.empty());
102 when(activeRulesHolder.get(ruleKey)).thenReturn(Optional.of(activeRule));
103 when(activeRule.getQProfileKey()).thenReturn("qpKey");
104 when(issue.getRuleKey()).thenReturn(ruleKey);
105 when(qProfileStatusRepository.get(any())).thenReturn(Optional.of(UNCHANGED));
109 public void should_not_backdate_if_no_scm_available() {
110 previousAnalysisWas(2000L);
111 currentAnalysisIs(3000L);
115 setRuleUpdatedAt(2800L);
119 assertNoChangeOfCreationDate();
123 @UseDataProvider("backdatingDateCases")
124 public void should_not_backdate_if_rule_and_plugin_and_base_plugin_are_old(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
125 previousAnalysisWas(2000L);
126 currentAnalysisIs(3000L);
129 configure.accept(issue, createMockScmInfo());
130 setRuleUpdatedAt(1500L);
131 rulePlugin("customjava");
132 pluginUpdatedAt("customjava", "java", 1700L);
133 pluginUpdatedAt("java", 1700L);
137 assertNoChangeOfCreationDate();
141 @UseDataProvider("backdatingDateCases")
142 public void should_not_backdate_if_rule_and_plugin_are_old_and_no_base_plugin(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
143 previousAnalysisWas(2000L);
144 currentAnalysisIs(3000L);
147 configure.accept(issue, createMockScmInfo());
148 setRuleUpdatedAt(1500L);
150 pluginUpdatedAt("java", 1700L);
154 assertNoChangeOfCreationDate();
158 @UseDataProvider("backdatingDateCases")
159 public void should_not_backdate_if_issue_existed_before(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
160 previousAnalysisWas(2000L);
161 currentAnalysisIs(3000L);
164 configure.accept(issue, createMockScmInfo());
165 setRuleUpdatedAt(2800L);
169 assertNoChangeOfCreationDate();
173 public void should_not_fail_for_issue_about_to_be_closed() {
174 previousAnalysisWas(2000L);
175 currentAnalysisIs(3000L);
178 setIssueBelongToNonExistingRule();
182 assertNoChangeOfCreationDate();
186 public void should_fail_if_rule_is_not_found() {
187 previousAnalysisWas(2000L);
188 currentAnalysisIs(3000L);
190 when(ruleRepository.findByKey(ruleKey)).thenReturn(Optional.empty());
193 assertThatThrownBy(this::run)
194 .isInstanceOf(IllegalStateException.class)
195 .hasMessage("The rule with key 'reop:rule' raised an issue, but no rule with that key was found");
199 @UseDataProvider("backdatingDateCases")
200 public void should_backdate_date_if_scm_is_available_and_rule_is_new(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
201 previousAnalysisWas(2000L);
202 currentAnalysisIs(3000L);
205 configure.accept(issue, createMockScmInfo());
206 setRuleUpdatedAt(2800L);
210 assertChangeOfCreationDateTo(expectedDate);
214 @UseDataProvider("backdatingDateCases")
215 public void should_backdate_date_if_scm_is_available_and_rule_has_changed(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
216 previousAnalysisWas(2000L);
217 currentAnalysisIs(3000L);
220 configure.accept(issue, createMockScmInfo());
221 setRuleUpdatedAt(2800L);
225 assertChangeOfCreationDateTo(expectedDate);
229 @UseDataProvider("backdatingDateCases")
230 public void should_backdate_date_if_scm_is_available_and_first_analysis(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
231 currentAnalysisIsFirstAnalysis();
232 currentAnalysisIs(3000L);
235 configure.accept(issue, createMockScmInfo());
239 assertChangeOfCreationDateTo(expectedDate);
243 @UseDataProvider("backdatingDateCases")
244 public void should_backdate_date_if_scm_is_available_and_current_component_is_new_file(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
245 previousAnalysisWas(2000L);
246 currentAnalysisIs(3000L);
249 configure.accept(issue, createMockScmInfo());
250 currentComponentIsNewFile();
254 assertChangeOfCreationDateTo(expectedDate);
258 @UseDataProvider("backdatingDateAndChangedQPStatusCases")
259 public void should_backdate_if_qp_of_the_rule_which_raised_the_issue_has_changed(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate, QProfileStatusRepository.Status status) {
260 previousAnalysisWas(2000L);
261 currentAnalysisIs(3000L);
264 configure.accept(issue, createMockScmInfo());
265 changeQualityProfile(status);
269 assertChangeOfCreationDateTo(expectedDate);
273 @UseDataProvider("backdatingDateCases")
274 public void should_backdate_if_scm_is_available_and_plugin_is_new(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
275 previousAnalysisWas(2000L);
276 currentAnalysisIs(3000L);
279 configure.accept(issue, createMockScmInfo());
280 setRuleUpdatedAt(1500L);
282 pluginUpdatedAt("java", 2500L);
286 assertChangeOfCreationDateTo(expectedDate);
290 @UseDataProvider("backdatingDateCases")
291 public void should_backdate_if_scm_is_available_and_base_plugin_is_new(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
292 previousAnalysisWas(2000L);
293 currentAnalysisIs(3000L);
296 configure.accept(issue, createMockScmInfo());
297 setRuleUpdatedAt(1500L);
298 rulePlugin("customjava");
299 pluginUpdatedAt("customjava", "java", 1000L);
300 pluginUpdatedAt("java", 2500L);
304 assertChangeOfCreationDateTo(expectedDate);
308 @UseDataProvider("backdatingDateCases")
309 public void should_backdate_external_issues(BiConsumer<DefaultIssue, ScmInfo> configure, long expectedDate) {
310 currentAnalysisIsFirstAnalysis();
311 currentAnalysisIs(3000L);
314 when(rule.isExternal()).thenReturn(true);
315 configure.accept(issue, createMockScmInfo());
319 assertChangeOfCreationDateTo(expectedDate);
320 verifyNoInteractions(activeRulesHolder);
324 public static Object[][] backdatingDateAndChangedQPStatusCases() {
325 return Stream.of(backdatingDateCases())
326 .flatMap(datesCases ->
327 Stream.of(QProfileStatusRepository.Status.values())
328 .filter(s -> !UNCHANGED.equals(s))
329 .map(s -> ArrayUtils.add(datesCases, s)))
330 .toArray(Object[][]::new);
334 public static Object[][] backdatingDateCases() {
335 return new Object[][] {
336 {new NoIssueLocation(), 1200L},
337 {new OnlyPrimaryLocation(), 1300L},
338 {new FlowOnCurrentFileOnly(), 1900L},
339 {new FlowOnMultipleFiles(), 1700L}
343 private static class NoIssueLocation implements BiConsumer<DefaultIssue, ScmInfo> {
345 public void accept(DefaultIssue issue, ScmInfo scmInfo) {
346 setDateOfLatestScmChangeset(scmInfo, 1200L);
350 private static class OnlyPrimaryLocation implements BiConsumer<DefaultIssue, ScmInfo> {
352 public void accept(DefaultIssue issue, ScmInfo scmInfo) {
353 when(issue.getLocations()).thenReturn(DbIssues.Locations.newBuilder().setTextRange(range(2, 3)).build());
354 setDateOfChangetsetAtLine(scmInfo, 2, 1200L);
355 setDateOfChangetsetAtLine(scmInfo, 3, 1300L);
359 private static class FlowOnCurrentFileOnly implements BiConsumer<DefaultIssue, ScmInfo> {
361 public void accept(DefaultIssue issue, ScmInfo scmInfo) {
362 Builder locations = DbIssues.Locations.newBuilder()
363 .setTextRange(range(2, 3))
364 .addFlow(newFlow(newLocation(4, 5)))
365 .addFlow(newFlow(newLocation(6, 7, COMPONENT_UUID), newLocation(8, 9, COMPONENT_UUID)));
366 when(issue.getLocations()).thenReturn(locations.build());
367 setDateOfChangetsetAtLine(scmInfo, 2, 1200L);
368 setDateOfChangetsetAtLine(scmInfo, 3, 1300L);
369 setDateOfChangetsetAtLine(scmInfo, 4, 1400L);
370 setDateOfChangetsetAtLine(scmInfo, 5, 1500L);
371 // some lines missing should be ok
372 setDateOfChangetsetAtLine(scmInfo, 9, 1900L);
376 private static class FlowOnMultipleFiles implements BiConsumer<DefaultIssue, ScmInfo> {
378 public void accept(DefaultIssue issue, ScmInfo scmInfo) {
379 Builder locations = DbIssues.Locations.newBuilder()
380 .setTextRange(range(2, 3))
381 .addFlow(newFlow(newLocation(4, 5)))
382 .addFlow(newFlow(newLocation(6, 7, COMPONENT_UUID), newLocation(8, 9, "another")));
383 when(issue.getLocations()).thenReturn(locations.build());
384 setDateOfChangetsetAtLine(scmInfo, 2, 1200L);
385 setDateOfChangetsetAtLine(scmInfo, 3, 1300L);
386 setDateOfChangetsetAtLine(scmInfo, 4, 1400L);
387 setDateOfChangetsetAtLine(scmInfo, 5, 1500L);
388 setDateOfChangetsetAtLine(scmInfo, 6, 1600L);
389 setDateOfChangetsetAtLine(scmInfo, 7, 1700L);
390 setDateOfChangetsetAtLine(scmInfo, 8, 1800L);
391 setDateOfChangetsetAtLine(scmInfo, 9, 1900L);
395 private void previousAnalysisWas(long analysisDate) {
396 analysisMetadataHolder.setBaseAnalysis(baseAnalysis);
397 when(baseAnalysis.getCreatedAt())
398 .thenReturn(analysisDate);
401 private void pluginUpdatedAt(String pluginKey, long updatedAt) {
402 scannerPlugins.put(pluginKey, new ScannerPlugin(pluginKey, null, updatedAt));
405 private void pluginUpdatedAt(String pluginKey, String basePluginKey, long updatedAt) {
406 scannerPlugins.put(pluginKey, new ScannerPlugin(pluginKey, basePluginKey, updatedAt));
409 private AnalysisMetadataHolderRule currentAnalysisIsFirstAnalysis() {
410 return analysisMetadataHolder.setBaseAnalysis(null);
413 private void currentAnalysisIs(long analysisDate) {
414 analysisMetadataHolder.setAnalysisDate(analysisDate);
417 private void currentComponentIsNewFile() {
418 when(component.getType()).thenReturn(Component.Type.FILE);
419 when(addedFileRepository.isAdded(component)).thenReturn(true);
422 private void makeIssueNew() {
427 private void makeIssueNotNew() {
432 private void changeQualityProfile(QProfileStatusRepository.Status status) {
433 when(qProfileStatusRepository.get(any())).thenReturn(Optional.of(status));
436 private void setIssueBelongToNonExistingRule() {
437 when(issue.getRuleKey())
438 .thenReturn(RuleKey.of("repo", "disabled"));
441 private void noScm() {
442 when(scmInfoRepository.getScmInfo(component))
443 .thenReturn(Optional.empty());
446 private static void setDateOfLatestScmChangeset(ScmInfo scmInfo, long date) {
447 Changeset changeset = Changeset.newChangesetBuilder().setDate(date).setRevision("1").build();
448 when(scmInfo.getLatestChangeset()).thenReturn(changeset);
451 private static void setDateOfChangetsetAtLine(ScmInfo scmInfo, int line, long date) {
452 Changeset changeset = Changeset.newChangesetBuilder().setDate(date).setRevision("1").build();
453 when(scmInfo.hasChangesetForLine(line)).thenReturn(true);
454 when(scmInfo.getChangesetForLine(line)).thenReturn(changeset);
457 private ScmInfo createMockScmInfo() {
458 if (scmInfo == null) {
459 scmInfo = mock(ScmInfo.class);
460 when(scmInfoRepository.getScmInfo(component))
461 .thenReturn(Optional.of(scmInfo));
466 private void setRuleUpdatedAt(long updateAt) {
467 when(activeRule.getUpdatedAt()).thenReturn(updateAt);
470 private void rulePlugin(String pluginKey) {
471 when(activeRule.getPluginKey()).thenReturn(pluginKey);
474 private static Location newLocation(int startLine, int endLine) {
475 return Location.newBuilder().setTextRange(range(startLine, endLine)).build();
478 private static Location newLocation(int startLine, int endLine, String componentUuid) {
479 return Location.newBuilder().setTextRange(range(startLine, endLine)).setComponentId(componentUuid).build();
482 private static org.sonar.db.protobuf.DbCommons.TextRange range(int startLine, int endLine) {
483 return TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build();
486 private static Flow newFlow(Location... locations) {
487 Flow.Builder builder = Flow.newBuilder();
488 Arrays.stream(locations).forEach(builder::addLocation);
489 return builder.build();
493 underTest.beforeComponent(component);
494 underTest.onIssue(component, issue);
495 underTest.afterComponent(component);
498 private void assertNoChangeOfCreationDate() {
499 verify(issueUpdater, never())
500 .setCreationDate(any(), any(), any());
503 private void assertChangeOfCreationDateTo(long createdAt) {
504 verify(issueUpdater, atLeastOnce())
505 .setCreationDate(same(issue), eq(new Date(createdAt)), any());