You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComponentIssuesLoaderTest.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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. import com.google.common.collect.ImmutableList;
  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.stream.IntStream;
  32. import javax.annotation.Nullable;
  33. import org.junit.Rule;
  34. import org.junit.Test;
  35. import org.junit.runner.RunWith;
  36. import org.sonar.api.config.Configuration;
  37. import org.sonar.api.config.internal.MapSettings;
  38. import org.sonar.api.issue.Issue;
  39. import org.sonar.api.utils.System2;
  40. import org.sonar.core.issue.DefaultIssue;
  41. import org.sonar.core.issue.FieldDiffs;
  42. import org.sonar.db.DbClient;
  43. import org.sonar.db.DbTester;
  44. import org.sonar.db.component.ComponentDto;
  45. import org.sonar.db.component.ComponentTesting;
  46. import org.sonar.db.issue.IssueDto;
  47. import org.sonar.db.rule.RuleDefinitionDto;
  48. import static java.util.Collections.emptyList;
  49. import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
  50. import static org.assertj.core.api.Assertions.assertThat;
  51. import static org.mockito.Mockito.mock;
  52. import static org.mockito.Mockito.verifyZeroInteractions;
  53. import static org.mockito.Mockito.when;
  54. import static org.sonar.api.issue.Issue.STATUS_CLOSED;
  55. import static org.sonar.api.rules.RuleType.CODE_SMELL;
  56. import static org.sonar.api.utils.DateUtils.addDays;
  57. import static org.sonar.api.utils.DateUtils.parseDateTime;
  58. @RunWith(DataProviderRunner.class)
  59. public class ComponentIssuesLoaderTest {
  60. private static final Date NOW = parseDateTime("2018-08-17T13:44:53+0000");
  61. private static final Date DATE_LIMIT_30_DAYS_BACK_MIDNIGHT = parseDateTime("2018-07-18T00:00:00+0000");
  62. @Rule
  63. public DbTester db = DbTester.create(System2.INSTANCE);
  64. private DbClient dbClient = db.getDbClient();
  65. private System2 system2 = mock(System2.class);
  66. @Test
  67. public void loadClosedIssues_returns_single_DefaultIssue_by_issue_based_on_first_row() {
  68. ComponentDto project = db.components().insertPublicProject();
  69. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  70. RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
  71. Date issueDate = addDays(NOW, -10);
  72. IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
  73. db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(issueDate, 10));
  74. db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 3), 20));
  75. db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 1), 30));
  76. when(system2.now()).thenReturn(NOW.getTime());
  77. ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
  78. List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
  79. assertThat(defaultIssues).hasSize(1);
  80. assertThat(defaultIssues.iterator().next().getLine()).isEqualTo(20);
  81. }
  82. @Test
  83. public void loadClosedIssues_returns_single_DefaultIssue_with_null_line_if_first_row_has_no_line_diff() {
  84. ComponentDto project = db.components().insertPublicProject();
  85. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  86. RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
  87. Date issueDate = addDays(NOW, -10);
  88. IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
  89. db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(issueDate, 10));
  90. db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 2), null));
  91. db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 1), 30));
  92. when(system2.now()).thenReturn(NOW.getTime());
  93. ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
  94. List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
  95. assertThat(defaultIssues).hasSize(1);
  96. assertThat(defaultIssues.iterator().next().getLine()).isNull();
  97. }
  98. @Test
  99. public void loadClosedIssues_returns_only_closed_issues_with_close_date() {
  100. ComponentDto project = db.components().insertPublicProject();
  101. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  102. RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
  103. Date issueDate = addDays(NOW, -10);
  104. IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
  105. db.issues().insertFieldDiffs(closedIssue, newToClosedDiffsWithLine(issueDate, 10));
  106. IssueDto issueNoCloseDate = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED));
  107. db.issues().insertFieldDiffs(issueNoCloseDate, newToClosedDiffsWithLine(issueDate, 10));
  108. when(system2.now()).thenReturn(NOW.getTime());
  109. ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
  110. List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
  111. assertThat(defaultIssues)
  112. .extracting(DefaultIssue::key)
  113. .containsOnly(closedIssue.getKey());
  114. }
  115. @Test
  116. public void loadClosedIssues_returns_only_closed_issues_which_close_date_is_from_day_30_days_ago() {
  117. ComponentIssuesLoader underTest = newComponentIssuesLoader(newEmptySettings());
  118. loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
  119. }
  120. @Test
  121. public void loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago_if_property_is_empty() {
  122. Configuration configuration = newConfiguration(null);
  123. ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
  124. loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
  125. }
  126. @Test
  127. public void loadClosedIssues_returns_only_closed_with_close_date_is_from_30_days_ago_if_property_is_less_than_0() {
  128. Configuration configuration = newConfiguration(String.valueOf(-(1 + new Random().nextInt(10))));
  129. ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
  130. loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
  131. }
  132. @Test
  133. public void loadClosedIssues_returns_only_closed_with_close_date_is_from_30_days_ago_if_property_is_30() {
  134. Configuration configuration = newConfiguration("30");
  135. ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
  136. loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
  137. }
  138. @Test
  139. @UseDataProvider("notAnIntegerPropertyValues")
  140. public void loadClosedIssues_returns_only_closed_with_close_date_is_from_30_days_ago_if_property_is_not_an_integer(String notAnInteger) {
  141. Configuration configuration = newConfiguration(notAnInteger);
  142. ComponentIssuesLoader underTest = newComponentIssuesLoader(configuration);
  143. loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(underTest);
  144. }
  145. @DataProvider
  146. public static Object[][] notAnIntegerPropertyValues() {
  147. return new Object[][] {
  148. {"foo"},
  149. {"1,3"},
  150. {"1.3"},
  151. {"-3.14"}
  152. };
  153. }
  154. private void loadClosedIssues_returns_only_closed_issues_with_close_date_is_from_30_days_ago(ComponentIssuesLoader underTest) {
  155. ComponentDto project = db.components().insertPublicProject();
  156. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  157. RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
  158. Date[] issueDates = new Date[] {
  159. addDays(NOW, -10),
  160. addDays(NOW, -31),
  161. addDays(NOW, -30),
  162. DATE_LIMIT_30_DAYS_BACK_MIDNIGHT,
  163. addDays(NOW, -29),
  164. addDays(NOW, -60),
  165. };
  166. IssueDto[] issues = Arrays.stream(issueDates)
  167. .map(issueDate -> {
  168. IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
  169. db.issues().insertFieldDiffs(closedIssue, newToClosedDiffsWithLine(issueDate, 10));
  170. return closedIssue;
  171. })
  172. .toArray(IssueDto[]::new);
  173. when(system2.now()).thenReturn(NOW.getTime());
  174. List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid());
  175. assertThat(defaultIssues)
  176. .extracting(DefaultIssue::key)
  177. .containsOnly(issues[0].getKey(), issues[2].getKey(), issues[3].getKey(), issues[4].getKey());
  178. }
  179. @Test
  180. public void loadClosedIssues_returns_empty_without_querying_DB_if_property_is_0() {
  181. System2 system2 = mock(System2.class);
  182. DbClient dbClient = mock(DbClient.class);
  183. Configuration configuration = newConfiguration("0");
  184. String componentUuid = randomAlphabetic(15);
  185. ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
  186. null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */, configuration, system2);
  187. assertThat(underTest.loadClosedIssues(componentUuid)).isEmpty();
  188. verifyZeroInteractions(dbClient, system2);
  189. }
  190. @Test
  191. public void loadLatestDiffChangesForReopeningOfClosedIssues_does_not_query_DB_if_issue_list_is_empty() {
  192. DbClient dbClient = mock(DbClient.class);
  193. ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
  194. null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
  195. underTest.loadLatestDiffChangesForReopeningOfClosedIssues(emptyList());
  196. verifyZeroInteractions(dbClient, system2);
  197. }
  198. @Test
  199. @UseDataProvider("statusOrResolutionFieldName")
  200. public void loadLatestDiffChangesForReopeningOfClosedIssues_add_diff_change_with_most_recent_status_or_resolution(String statusOrResolutionFieldName) {
  201. ComponentDto project = db.components().insertPublicProject();
  202. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  203. RuleDefinitionDto rule = db.rules().insert();
  204. IssueDto issue = db.issues().insert(rule, project, file);
  205. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val1")).setIssueChangeCreationDate(5));
  206. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val2")).setIssueChangeCreationDate(20));
  207. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val3")).setIssueChangeCreationDate(13));
  208. ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
  209. null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
  210. DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
  211. underTest.loadLatestDiffChangesForReopeningOfClosedIssues(ImmutableList.of(defaultIssue));
  212. assertThat(defaultIssue.changes())
  213. .hasSize(1);
  214. assertThat(defaultIssue.changes())
  215. .extracting(t -> t.get(statusOrResolutionFieldName))
  216. .filteredOn(t -> hasValue(t, "val2"))
  217. .hasSize(1);
  218. }
  219. @Test
  220. public void loadLatestDiffChangesForReopeningOfClosedIssues_add_single_diff_change_when_most_recent_status_and_resolution_is_the_same_diff() {
  221. ComponentDto project = db.components().insertPublicProject();
  222. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  223. RuleDefinitionDto rule = db.rules().insert();
  224. IssueDto issue = db.issues().insert(rule, project, file);
  225. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus1")).setIssueChangeCreationDate(5));
  226. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus2")).setIssueChangeCreationDate(19));
  227. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus3", "resolution", "valRes3")).setIssueChangeCreationDate(20));
  228. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("resolution", "valRes4")).setIssueChangeCreationDate(13));
  229. ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
  230. null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
  231. DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
  232. underTest.loadLatestDiffChangesForReopeningOfClosedIssues(ImmutableList.of(defaultIssue));
  233. assertThat(defaultIssue.changes())
  234. .hasSize(1);
  235. assertThat(defaultIssue.changes())
  236. .extracting(t -> t.get("status"))
  237. .filteredOn(t -> hasValue(t, "valStatus3"))
  238. .hasSize(1);
  239. assertThat(defaultIssue.changes())
  240. .extracting(t -> t.get("resolution"))
  241. .filteredOn(t -> hasValue(t, "valRes3"))
  242. .hasSize(1);
  243. }
  244. @Test
  245. public void loadLatestDiffChangesForReopeningOfClosedIssues_adds_2_diff_changes_if_most_recent_status_and_resolution_are_not_the_same_diff() {
  246. ComponentDto project = db.components().insertPublicProject();
  247. ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
  248. RuleDefinitionDto rule = db.rules().insert();
  249. IssueDto issue = db.issues().insert(rule, project, file);
  250. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus1")).setIssueChangeCreationDate(5));
  251. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus2", "resolution", "valRes2")).setIssueChangeCreationDate(19));
  252. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus3")).setIssueChangeCreationDate(20));
  253. db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("resolution", "valRes4")).setIssueChangeCreationDate(13));
  254. ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
  255. null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
  256. DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
  257. underTest.loadLatestDiffChangesForReopeningOfClosedIssues(ImmutableList.of(defaultIssue));
  258. assertThat(defaultIssue.changes())
  259. .hasSize(2);
  260. assertThat(defaultIssue.changes())
  261. .extracting(t -> t.get("status"))
  262. .filteredOn(t -> hasValue(t, "valStatus3"))
  263. .hasSize(1);
  264. assertThat(defaultIssue.changes())
  265. .extracting(t -> t.get("resolution"))
  266. .filteredOn(t -> hasValue(t, "valRes2"))
  267. .hasSize(1);
  268. }
  269. private static boolean hasValue(@Nullable FieldDiffs.Diff t, String value) {
  270. if (t == null) {
  271. return false;
  272. }
  273. return (t.oldValue() == null || value.equals(t.oldValue())) && (t.newValue() == null || value.equals(t.newValue()));
  274. }
  275. @DataProvider
  276. public static Object[][] statusOrResolutionFieldName() {
  277. return new Object[][] {
  278. {"status"},
  279. {"resolution"},
  280. };
  281. }
  282. private static String randomDiffWith(String... fieldsAndValues) {
  283. Random random = new Random();
  284. List<Diff> diffs = new ArrayList<>();
  285. for (int i = 0; i < fieldsAndValues.length; i++) {
  286. int oldOrNew = random.nextInt(3);
  287. String value = fieldsAndValues[i + 1];
  288. diffs.add(new Diff(fieldsAndValues[i], oldOrNew <= 2 ? value : null, oldOrNew >= 2 ? value : null));
  289. i++;
  290. }
  291. IntStream.range(0, random.nextInt(5))
  292. .forEach(i -> diffs.add(new Diff(randomAlphabetic(10), random.nextBoolean() ? null : randomAlphabetic(11), random.nextBoolean() ? null : randomAlphabetic(12))));
  293. Collections.shuffle(diffs);
  294. FieldDiffs res = new FieldDiffs();
  295. diffs.forEach(diff -> res.setDiff(diff.field, diff.oldValue, diff.newValue));
  296. return res.toEncodedString();
  297. }
  298. private static final class Diff {
  299. private final String field;
  300. private final String oldValue;
  301. private final String newValue;
  302. private Diff(String field, @Nullable String oldValue, @Nullable String newValue) {
  303. this.field = field;
  304. this.oldValue = oldValue;
  305. this.newValue = newValue;
  306. }
  307. }
  308. private static FieldDiffs newToClosedDiffsWithLine(Date creationDate, @Nullable Integer oldLineValue) {
  309. FieldDiffs fieldDiffs = new FieldDiffs().setCreationDate(addDays(creationDate, -5))
  310. .setDiff("status", randomNonCloseStatus(), STATUS_CLOSED);
  311. if (oldLineValue != null) {
  312. fieldDiffs.setDiff("line", oldLineValue, "");
  313. }
  314. return fieldDiffs;
  315. }
  316. private static String randomNonCloseStatus() {
  317. String[] nonCloseStatuses = Issue.STATUSES.stream()
  318. .filter(t -> !STATUS_CLOSED.equals(t))
  319. .toArray(String[]::new);
  320. return nonCloseStatuses[new Random().nextInt(nonCloseStatuses.length)];
  321. }
  322. private ComponentIssuesLoader newComponentIssuesLoader(Configuration configuration) {
  323. return new ComponentIssuesLoader(dbClient,
  324. null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */, configuration, system2);
  325. }
  326. private static Configuration newEmptySettings() {
  327. return new MapSettings().asConfig();
  328. }
  329. private static Configuration newConfiguration(@Nullable String maxAge) {
  330. MapSettings settings = new MapSettings();
  331. settings.setProperty("sonar.issuetracking.closedissues.maxage", maxAge);
  332. return settings.asConfig();
  333. }
  334. }