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.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.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.Date;
29 import java.util.List;
30 import java.util.Random;
31 import java.util.function.Consumer;
32 import java.util.stream.IntStream;
33 import java.util.stream.LongStream;
34 import javax.annotation.Nullable;
35 import org.junit.Rule;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 import org.sonar.api.config.Configuration;
39 import org.sonar.api.config.internal.MapSettings;
40 import org.sonar.api.issue.Issue;
41 import org.sonar.api.utils.System2;
42 import org.sonar.core.issue.DefaultIssue;
43 import org.sonar.core.issue.DefaultIssueComment;
44 import org.sonar.core.issue.FieldDiffs;
45 import org.sonar.db.DbClient;
46 import org.sonar.db.DbTester;
47 import org.sonar.db.component.ComponentDto;
48 import org.sonar.db.component.ComponentTesting;
49 import org.sonar.db.issue.IssueChangeDto;
50 import org.sonar.db.issue.IssueDto;
51 import org.sonar.db.rule.RuleDefinitionDto;
53 import static java.util.Collections.emptyList;
54 import static java.util.Collections.singleton;
55 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
56 import static org.assertj.core.api.Assertions.assertThat;
57 import static org.mockito.Mockito.mock;
58 import static org.mockito.Mockito.verifyNoInteractions;
59 import static org.mockito.Mockito.when;
60 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
61 import static org.sonar.api.rules.RuleType.CODE_SMELL;
62 import static org.sonar.api.utils.DateUtils.addDays;
63 import static org.sonar.api.utils.DateUtils.parseDateTime;
64 import static org.sonar.ce.task.projectanalysis.issue.ComponentIssuesLoader.NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP;
66 @RunWith(DataProviderRunner.class)
67 public class ComponentIssuesLoaderTest {
68 private static final Date NOW = parseDateTime("2018-08-17T13:44:53+0000");
69 private static final Date DATE_LIMIT_30_DAYS_BACK_MIDNIGHT = parseDateTime("2018-07-18T00:00:00+0000");
72 public DbTester db = DbTester.create(System2.INSTANCE);
74 private final DbClient dbClient = db.getDbClient();
75 private final System2 system2 = mock(System2.class);
76 private final IssueChangesToDeleteRepository issueChangesToDeleteRepository = new IssueChangesToDeleteRepository();
79 public void loadClosedIssues_returns_single_DefaultIssue_by_issue_based_on_first_row() {
80 ComponentDto project = db.components().insertPublicProject();
81 ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
82 RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
83 Date issueDate = addDays(NOW, -10);
84 IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
85 db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(issueDate, 10));
86 db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 3), 20));
87 db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 1), 30));
88 when(system2.now()).thenReturn(NOW.getTime());
90 ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
91 List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
93 assertThat(defaultIssues).hasSize(1);
94 assertThat(defaultIssues.iterator().next().getLine()).isEqualTo(20);
98 public void loadClosedIssues_returns_single_DefaultIssue_with_null_line_if_first_row_has_no_line_diff() {
99 ComponentDto project = db.components().insertPublicProject();
100 ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
101 RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
102 Date issueDate = addDays(NOW, -10);
103 IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
104 db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(issueDate, 10));
105 db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 2), null));
106 db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 1), 30));
107 when(system2.now()).thenReturn(NOW.getTime());
109 ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
110 List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
112 assertThat(defaultIssues).hasSize(1);
113 assertThat(defaultIssues.iterator().next().getLine()).isNull();
117 public void loadClosedIssues_returns_only_closed_issues_with_close_date() {
118 ComponentDto project = db.components().insertPublicProject();
119 ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
120 RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
121 Date issueDate = addDays(NOW, -10);
122 IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
123 db.issues().insertFieldDiffs(closedIssue, newToClosedDiffsWithLine(issueDate, 10));
124 IssueDto issueNoCloseDate = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED));
125 db.issues().insertFieldDiffs(issueNoCloseDate, newToClosedDiffsWithLine(issueDate, 10));
126 when(system2.now()).thenReturn(NOW.getTime());
128 ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
129 List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
131 assertThat(defaultIssues)
132 .extracting(DefaultIssue::key)
133 .containsOnly(closedIssue.getKey());
137 public void loadClosedIssues_returns_only_closed_issues_which_close_date_is_from_day_30_days_ago() {
138 ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
139 loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
143 public void loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago_if_property_is_empty() {
144 Configuration configuration = newConfiguration(null);
145 ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
147 loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
151 public void loadClosedIssues_returns_only_closed_with_close_date_is_from_30_days_ago_if_property_is_less_than_0() {
152 Configuration configuration = newConfiguration(String.valueOf(-(1 + new Random().nextInt(10))));
153 ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
155 loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
159 public void loadClosedIssues_returns_only_closed_with_close_date_is_from_30_days_ago_if_property_is_30() {
160 Configuration configuration = newConfiguration("30");
161 ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
163 loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
167 @UseDataProvider("notAnIntegerPropertyValues")
168 public void loadClosedIssues_returns_only_closed_with_close_date_is_from_30_days_ago_if_property_is_not_an_integer(String notAnInteger) {
169 Configuration configuration = newConfiguration(notAnInteger);
170 ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
172 loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
176 public static Object[][] notAnIntegerPropertyValues() {
177 return new Object[][] {
185 private void loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(ComponentIssuesLoader underTest) {
186 ComponentDto project = db.components().insertPublicProject();
187 ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
188 RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
189 Date[] issueDates = new Date[] {
193 DATE_LIMIT_30_DAYS_BACK_MIDNIGHT,
197 IssueDto[] issues = Arrays.stream(issueDates)
199 IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
200 db.issues().insertFieldDiffs(closedIssue, newToClosedDiffsWithLine(issueDate, 10));
203 .toArray(IssueDto[]::new);
204 when(system2.now()).thenReturn(NOW.getTime());
206 List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
208 assertThat(defaultIssues)
209 .extracting(DefaultIssue::key)
210 .containsOnly(issues[0].getKey(), issues[2].getKey(), issues[3].getKey(), issues[4].getKey());
214 public void loadClosedIssues_returns_empty_without_querying_DB_if_property_is_0() {
215 System2 system2 = mock(System2.class);
216 DbClient dbClient = mock(DbClient.class);
217 Configuration configuration = newConfiguration("0");
218 String componentUuid = randomAlphabetic(15);
219 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, configuration, system2, issueChangesToDeleteRepository);
221 assertThat(underTest.loadClosedIssues(componentUuid)).isEmpty();
223 verifyNoInteractions(dbClient, system2);
227 public void loadLatestDiffChangesForReopeningOfClosedIssues_collects_issue_changes_to_delete() {
228 IssueDto issue = db.issues().insert();
229 for (long i = 0; i < NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 5; i++) {
230 db.issues().insertChange(issue, diffIssueChangeModifier(i, "status"));
232 // should not be deleted
233 db.issues().insertChange(issue, diffIssueChangeModifier(-1, "other"));
235 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
237 underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(new DefaultIssue().setKey(issue.getKey())));
238 assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0", "1", "2", "3", "4");
242 public void loadLatestDiffChangesForReopeningOfClosedIssues_does_not_query_DB_if_issue_list_is_empty() {
243 DbClient dbClient = mock(DbClient.class);
244 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
246 underTest.loadLatestDiffChangesForReopeningOfClosedIssues(emptyList());
248 verifyNoInteractions(dbClient, system2);
252 @UseDataProvider("statusOrResolutionFieldName")
253 public void loadLatestDiffChangesForReopeningOfClosedIssues_add_diff_change_with_most_recent_status_or_resolution(String statusOrResolutionFieldName) {
254 IssueDto issue = db.issues().insert();
255 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val1")).setIssueChangeCreationDate(5));
256 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val2")).setIssueChangeCreationDate(20));
257 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val3")).setIssueChangeCreationDate(13));
258 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
259 DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
261 underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
263 assertThat(defaultIssue.changes())
265 assertThat(defaultIssue.changes())
266 .extracting(t -> t.get(statusOrResolutionFieldName))
267 .filteredOn(t -> hasValue(t, "val2"))
272 public void loadLatestDiffChangesForReopeningOfClosedIssues_add_single_diff_change_when_most_recent_status_and_resolution_is_the_same_diff() {
273 IssueDto issue = db.issues().insert();
274 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus1")).setIssueChangeCreationDate(5));
275 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus2")).setIssueChangeCreationDate(19));
276 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus3", "resolution", "valRes3")).setIssueChangeCreationDate(20));
277 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("resolution", "valRes4")).setIssueChangeCreationDate(13));
278 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
279 DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
281 underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
283 assertThat(defaultIssue.changes())
285 assertThat(defaultIssue.changes())
286 .extracting(t -> t.get("status"))
287 .filteredOn(t -> hasValue(t, "valStatus3"))
289 assertThat(defaultIssue.changes())
290 .extracting(t -> t.get("resolution"))
291 .filteredOn(t -> hasValue(t, "valRes3"))
296 public void loadLatestDiffChangesForReopeningOfClosedIssues_adds_2_diff_changes_if_most_recent_status_and_resolution_are_not_the_same_diff() {
297 IssueDto issue = db.issues().insert();
298 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus1")).setIssueChangeCreationDate(5));
299 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus2", "resolution", "valRes2")).setIssueChangeCreationDate(19));
300 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus3")).setIssueChangeCreationDate(20));
301 db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("resolution", "valRes4")).setIssueChangeCreationDate(13));
302 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null /* not used in method */, null /* not used in method */,
303 newConfiguration("0"), null /* not used by method */, issueChangesToDeleteRepository);
304 DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
306 underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
308 assertThat(defaultIssue.changes())
310 assertThat(defaultIssue.changes())
311 .extracting(t -> t.get("status"))
312 .filteredOn(t -> hasValue(t, "valStatus3"))
314 assertThat(defaultIssue.changes())
315 .extracting(t -> t.get("resolution"))
316 .filteredOn(t -> hasValue(t, "valRes2"))
321 public void loadChanges_should_filter_out_old_status_changes() {
322 IssueDto issue = db.issues().insert();
323 for (int i = 0; i < NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1; i++) {
324 db.issues().insertChange(issue, diffIssueChangeModifier(i, "status"));
327 db.issues().insertChange(issue, diffIssueChangeModifier(NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1, "other"));
328 db.issues().insertChange(issue, t -> t
329 .setChangeType(IssueChangeDto.TYPE_COMMENT)
330 .setKey("comment1"));
332 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
333 DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
334 underTest.loadChanges(db.getSession(), singleton(defaultIssue));
336 assertThat(defaultIssue.changes())
337 .extracting(d -> d.creationDate().getTime())
338 .containsOnly(LongStream.rangeClosed(1, NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1).boxed().toArray(Long[]::new));
339 assertThat(defaultIssue.defaultIssueComments()).extracting(DefaultIssueComment::key).containsOnly("comment1");
340 assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0");
344 public void loadChanges_should_filter_out_old_from_branch_changes() {
345 IssueDto issue = db.issues().insert();
346 for (int i = 0; i < NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1; i++) {
347 db.issues().insertChange(issue, diffIssueChangeModifier(i, "from_branch"));
350 ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
351 DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
352 underTest.loadChanges(db.getSession(), singleton(defaultIssue));
353 assertThat(defaultIssue.changes())
354 .extracting(d -> d.creationDate().getTime())
355 .containsOnly(LongStream.rangeClosed(1, NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP).boxed().toArray(Long[]::new));
356 assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0");
359 private Consumer<IssueChangeDto> diffIssueChangeModifier(long created, String field) {
360 return issueChangeDto -> issueChangeDto
361 .setChangeData(new FieldDiffs().setDiff(field, "A", "B").toEncodedString())
362 .setIssueChangeCreationDate(created)
363 .setUuid(String.valueOf(created));
366 private static boolean hasValue(@Nullable FieldDiffs.Diff t, String value) {
370 return (t.oldValue() == null || value.equals(t.oldValue())) && (t.newValue() == null || value.equals(t.newValue()));
374 public static Object[][] statusOrResolutionFieldName() {
375 return new Object[][] {
381 private static String randomDiffWith(String... fieldsAndValues) {
382 Random random = new Random();
383 List<Diff> diffs = new ArrayList<>();
384 for (int i = 0; i < fieldsAndValues.length; i++) {
385 int oldOrNew = random.nextInt(3);
386 String value = fieldsAndValues[i + 1];
387 diffs.add(new Diff(fieldsAndValues[i], oldOrNew <= 2 ? value : null, oldOrNew >= 2 ? value : null));
390 IntStream.range(0, random.nextInt(5))
391 .forEach(i -> diffs.add(new Diff(randomAlphabetic(10), random.nextBoolean() ? null : randomAlphabetic(11), random.nextBoolean() ? null : randomAlphabetic(12))));
392 Collections.shuffle(diffs);
394 FieldDiffs res = new FieldDiffs();
395 diffs.forEach(diff -> res.setDiff(diff.field, diff.oldValue, diff.newValue));
396 return res.toEncodedString();
399 private static final class Diff {
400 private final String field;
401 private final String oldValue;
402 private final String newValue;
404 private Diff(String field, @Nullable String oldValue, @Nullable String newValue) {
406 this.oldValue = oldValue;
407 this.newValue = newValue;
411 private static FieldDiffs newToClosedDiffsWithLine(Date creationDate, @Nullable Integer oldLineValue) {
412 FieldDiffs fieldDiffs = new FieldDiffs().setCreationDate(addDays(creationDate, -5))
413 .setDiff("status", randomNonCloseStatus(), STATUS_CLOSED);
414 if (oldLineValue != null) {
415 fieldDiffs.setDiff("line", oldLineValue, "");
420 private static String randomNonCloseStatus() {
421 String[] nonCloseStatuses = Issue.STATUSES.stream()
422 .filter(t -> !STATUS_CLOSED.equals(t))
423 .toArray(String[]::new);
424 return nonCloseStatuses[new Random().nextInt(nonCloseStatuses.length)];
427 private ComponentIssuesLoader newComponentIssuesLoader(Configuration configuration) {
428 return new ComponentIssuesLoader(dbClient, null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */,
429 configuration, system2, issueChangesToDeleteRepository);
432 private static Configuration newEmptySettings() {
433 return new MapSettings().asConfig();
436 private static Configuration newConfiguration(@Nullable String maxAge) {
437 MapSettings settings = new MapSettings();
438 settings.setProperty("sonar.issuetracking.closedissues.maxage", maxAge);
439 return settings.asConfig();