]> source.dussan.org Git - sonarqube.git/blob
5ab14235cb4f6d52f5816cab8615f295c02a7ca6
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.ce.task.projectanalysis.issue;
21
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.RuleDto;
52
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;
65
66 @RunWith(DataProviderRunner.class)
67 public class ComponentIssuesLoaderIT {
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");
70
71   @Rule
72   public DbTester db = DbTester.create(System2.INSTANCE);
73
74   private final DbClient dbClient = db.getDbClient();
75   private final System2 system2 = mock(System2.class);
76   private final IssueChangesToDeleteRepository issueChangesToDeleteRepository = new IssueChangesToDeleteRepository();
77
78   @Test
79   public void loadClosedIssues_returns_single_DefaultIssue_by_issue_based_on_first_row() {
80     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
81     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
82     RuleDto 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());
89
90     ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
91     List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
92
93     assertThat(defaultIssues).hasSize(1);
94     assertThat(defaultIssues.iterator().next().getLine()).isEqualTo(20);
95   }
96
97   @Test
98   public void loadClosedIssues_returns_single_DefaultIssue_with_null_line_if_first_row_has_no_line_diff() {
99     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
100     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
101     RuleDto 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());
108
109     ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
110     List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
111
112     assertThat(defaultIssues).hasSize(1);
113     assertThat(defaultIssues.iterator().next().getLine()).isNull();
114   }
115
116   @Test
117   public void loadClosedIssues_returns_only_closed_issues_with_close_date() {
118     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
119     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
120     RuleDto 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());
127
128     ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
129     List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
130
131     assertThat(defaultIssues)
132       .extracting(DefaultIssue::key)
133       .containsOnly(closedIssue.getKey());
134   }
135
136   @Test
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);
140   }
141
142   @Test
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);
146
147     loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
148   }
149
150   @Test
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);
154
155     loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
156   }
157
158   @Test
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);
162
163     loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
164   }
165
166   @Test
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);
171
172     loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
173   }
174
175   @DataProvider
176   public static Object[][] notAnIntegerPropertyValues() {
177     return new Object[][] {
178       {"foo"},
179       {"1,3"},
180       {"1.3"},
181       {"-3.14"}
182     };
183   }
184
185   private void loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(ComponentIssuesLoader underTest) {
186     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
187     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
188     RuleDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
189     Date[] issueDates = new Date[] {
190       addDays(NOW, -10),
191       addDays(NOW, -31),
192       addDays(NOW, -30),
193       DATE_LIMIT_30_DAYS_BACK_MIDNIGHT,
194       addDays(NOW, -29),
195       addDays(NOW, -60),
196     };
197     IssueDto[] issues = Arrays.stream(issueDates)
198       .map(issueDate -> {
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));
201         return closedIssue;
202       })
203       .toArray(IssueDto[]::new);
204     when(system2.now()).thenReturn(NOW.getTime());
205
206     List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
207
208     assertThat(defaultIssues)
209       .extracting(DefaultIssue::key)
210       .containsOnly(issues[0].getKey(), issues[2].getKey(), issues[3].getKey(), issues[4].getKey());
211   }
212
213   @Test
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);
220
221     assertThat(underTest.loadClosedIssues(componentUuid)).isEmpty();
222
223     verifyNoInteractions(dbClient, system2);
224   }
225
226   @Test
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"));
231     }
232     // should not be deleted
233     db.issues().insertChange(issue, diffIssueChangeModifier(-1, "other"));
234
235     ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
236
237     underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(new DefaultIssue().setKey(issue.getKey())));
238     assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0", "1", "2", "3", "4");
239   }
240
241   @Test
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);
245
246     underTest.loadLatestDiffChangesForReopeningOfClosedIssues(emptyList());
247
248     verifyNoInteractions(dbClient, system2);
249   }
250
251   @Test
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());
260
261     underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
262
263     assertThat(defaultIssue.changes())
264       .hasSize(1);
265     assertThat(defaultIssue.changes())
266       .extracting(t -> t.get(statusOrResolutionFieldName))
267       .filteredOn(t -> hasValue(t, "val2"))
268       .hasSize(1);
269   }
270
271   @Test
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());
280
281     underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
282
283     assertThat(defaultIssue.changes())
284       .hasSize(1);
285     assertThat(defaultIssue.changes())
286       .extracting(t -> t.get("status"))
287       .filteredOn(t -> hasValue(t, "valStatus3"))
288       .hasSize(1);
289     assertThat(defaultIssue.changes())
290       .extracting(t -> t.get("resolution"))
291       .filteredOn(t -> hasValue(t, "valRes3"))
292       .hasSize(1);
293   }
294
295   @Test
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());
305
306     underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
307
308     assertThat(defaultIssue.changes())
309       .hasSize(2);
310     assertThat(defaultIssue.changes())
311       .extracting(t -> t.get("status"))
312       .filteredOn(t -> hasValue(t, "valStatus3"))
313       .hasSize(1);
314     assertThat(defaultIssue.changes())
315       .extracting(t -> t.get("resolution"))
316       .filteredOn(t -> hasValue(t, "valRes2"))
317       .hasSize(1);
318   }
319
320   @Test
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"));
325     }
326     // these are kept
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"));
331
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));
335
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");
341   }
342
343   @Test
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"));
348     }
349
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");
357   }
358
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));
364   }
365
366   private static boolean hasValue(@Nullable FieldDiffs.Diff t, String value) {
367     if (t == null) {
368       return false;
369     }
370     return (t.oldValue() == null || value.equals(t.oldValue())) && (t.newValue() == null || value.equals(t.newValue()));
371   }
372
373   @DataProvider
374   public static Object[][] statusOrResolutionFieldName() {
375     return new Object[][] {
376       {"status"},
377       {"resolution"},
378     };
379   }
380
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));
388       i++;
389     }
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);
393
394     FieldDiffs res = new FieldDiffs();
395     diffs.forEach(diff -> res.setDiff(diff.field, diff.oldValue, diff.newValue));
396     return res.toEncodedString();
397   }
398
399   private static final class Diff {
400     private final String field;
401     private final String oldValue;
402     private final String newValue;
403
404     private Diff(String field, @Nullable String oldValue, @Nullable String newValue) {
405       this.field = field;
406       this.oldValue = oldValue;
407       this.newValue = newValue;
408     }
409   }
410
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, "");
416     }
417     return fieldDiffs;
418   }
419
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)];
425   }
426
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);
430   }
431
432   private static Configuration newEmptySettings() {
433     return new MapSettings().asConfig();
434   }
435
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();
440   }
441 }