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.

PostProjectAnalysisTasksExecutorTest.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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.api.posttask;
  21. import com.google.common.collect.ImmutableMap;
  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.Date;
  26. import java.util.HashMap;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Random;
  30. import javax.annotation.Nullable;
  31. import org.apache.commons.lang3.RandomStringUtils;
  32. import org.junit.Before;
  33. import org.junit.Rule;
  34. import org.junit.Test;
  35. import org.junit.runner.RunWith;
  36. import org.mockito.ArgumentCaptor;
  37. import org.mockito.InOrder;
  38. import org.slf4j.event.Level;
  39. import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
  40. import org.sonar.api.ce.posttask.Project;
  41. import org.sonar.api.testfixtures.log.LogTester;
  42. import org.sonar.ce.task.CeTask;
  43. import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
  44. import org.sonar.ce.task.projectanalysis.analysis.Branch;
  45. import org.sonar.ce.task.projectanalysis.batch.BatchReportReaderRule;
  46. import org.sonar.ce.task.projectanalysis.metric.Metric;
  47. import org.sonar.ce.task.projectanalysis.qualitygate.Condition;
  48. import org.sonar.ce.task.projectanalysis.qualitygate.ConditionStatus;
  49. import org.sonar.ce.task.projectanalysis.qualitygate.MutableQualityGateHolderRule;
  50. import org.sonar.ce.task.projectanalysis.qualitygate.MutableQualityGateStatusHolderRule;
  51. import org.sonar.ce.task.projectanalysis.qualitygate.QualityGate;
  52. import org.sonar.ce.task.projectanalysis.qualitygate.QualityGateStatus;
  53. import org.sonar.db.component.BranchType;
  54. import org.sonar.scanner.protocol.output.ScannerReport;
  55. import static com.google.common.collect.ImmutableList.of;
  56. import static java.util.Arrays.asList;
  57. import static java.util.stream.Collectors.toList;
  58. import static org.assertj.core.api.Assertions.assertThat;
  59. import static org.assertj.core.api.ThrowableAssert.catchThrowable;
  60. import static org.assertj.core.data.MapEntry.entry;
  61. import static org.mockito.ArgumentMatchers.any;
  62. import static org.mockito.Mockito.doAnswer;
  63. import static org.mockito.Mockito.doThrow;
  64. import static org.mockito.Mockito.inOrder;
  65. import static org.mockito.Mockito.mock;
  66. import static org.mockito.Mockito.verify;
  67. import static org.mockito.Mockito.when;
  68. @RunWith(DataProviderRunner.class)
  69. public class PostProjectAnalysisTasksExecutorTest {
  70. private static final String QUALITY_GATE_UUID = "98451";
  71. private static final String QUALITY_GATE_NAME = "qualityGate name";
  72. private static final Condition CONDITION_1 = createCondition("metric key 1");
  73. private static final Condition CONDITION_2 = createCondition("metric key 2");
  74. @Rule
  75. public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
  76. @Rule
  77. public MutableQualityGateHolderRule qualityGateHolder = new MutableQualityGateHolderRule();
  78. @Rule
  79. public MutableQualityGateStatusHolderRule qualityGateStatusHolder = new MutableQualityGateStatusHolderRule();
  80. @Rule
  81. public BatchReportReaderRule reportReader = new BatchReportReaderRule();
  82. @Rule
  83. public LogTester logTester = new LogTester();
  84. private final ArgumentCaptor<PostProjectAnalysisTask.Context> taskContextCaptor = ArgumentCaptor.forClass(PostProjectAnalysisTask.Context.class);
  85. private final CeTask.Component component = new CeTask.Component("component uuid", "component key", "component name");
  86. private final CeTask.Component entity = new CeTask.Component("entity uuid", "component key", "component name");
  87. private final CeTask ceTask = new CeTask.Builder()
  88. .setType("type")
  89. .setUuid("uuid")
  90. .setComponent(component)
  91. .setEntity(entity)
  92. .build();
  93. private final PostProjectAnalysisTask postProjectAnalysisTask = newPostProjectAnalysisTask("PT1");
  94. private final PostProjectAnalysisTasksExecutor underTest = new PostProjectAnalysisTasksExecutor(
  95. ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder,
  96. reportReader, new PostProjectAnalysisTask[] {postProjectAnalysisTask});
  97. @Before
  98. public void setUp() {
  99. qualityGateHolder.setQualityGate(new QualityGate(QUALITY_GATE_UUID, QUALITY_GATE_NAME, of(CONDITION_1, CONDITION_2)));
  100. qualityGateStatusHolder.setStatus(QualityGateStatus.OK, ImmutableMap.of(
  101. CONDITION_1, ConditionStatus.create(ConditionStatus.EvaluationStatus.OK, "value"),
  102. CONDITION_2, ConditionStatus.NO_VALUE_STATUS));
  103. Branch branch = mock(Branch.class);
  104. when(branch.getType()).thenReturn(BranchType.BRANCH);
  105. analysisMetadataHolder
  106. .setBranch(branch);
  107. reportReader.setMetadata(ScannerReport.Metadata.newBuilder().build());
  108. }
  109. @Test
  110. @UseDataProvider("booleanValues")
  111. public void does_not_fail_when_there_is_no_PostProjectAnalysisTasksExecutor(boolean allStepsExecuted) {
  112. new PostProjectAnalysisTasksExecutor(ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader, null)
  113. .finished(allStepsExecuted);
  114. }
  115. @Test
  116. @UseDataProvider("booleanValues")
  117. public void finished_calls_all_PostProjectAnalysisTask_in_order_of_the_array_and_passes_the_same_object_to_all(boolean allStepsExecuted) {
  118. PostProjectAnalysisTask postProjectAnalysisTask1 = newPostProjectAnalysisTask("PT1");
  119. PostProjectAnalysisTask postProjectAnalysisTask2 = newPostProjectAnalysisTask("PT2");
  120. InOrder inOrder = inOrder(postProjectAnalysisTask1, postProjectAnalysisTask2);
  121. new PostProjectAnalysisTasksExecutor(
  122. ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader,
  123. new PostProjectAnalysisTask[] {postProjectAnalysisTask1, postProjectAnalysisTask2})
  124. .finished(allStepsExecuted);
  125. inOrder.verify(postProjectAnalysisTask1).finished(taskContextCaptor.capture());
  126. inOrder.verify(postProjectAnalysisTask1).getDescription();
  127. inOrder.verify(postProjectAnalysisTask2).finished(taskContextCaptor.capture());
  128. inOrder.verify(postProjectAnalysisTask2).getDescription();
  129. inOrder.verifyNoMoreInteractions();
  130. ArgumentCaptor<PostProjectAnalysisTask.Context> taskContextCaptor = this.taskContextCaptor;
  131. List<PostProjectAnalysisTask.ProjectAnalysis> allValues = getAllProjectAnalyses(taskContextCaptor);
  132. assertThat(allValues).hasSize(2);
  133. assertThat(allValues.get(0)).isSameAs(allValues.get(1));
  134. assertThat(logTester.logs()).hasSize(2);
  135. List<String> logs = logTester.logs(Level.INFO);
  136. assertThat(logs).hasSize(2);
  137. assertThat(logs.get(0)).matches("^PT1 \\| status=SUCCESS \\| time=\\d+ms$");
  138. assertThat(logs.get(1)).matches("^PT2 \\| status=SUCCESS \\| time=\\d+ms$");
  139. }
  140. @Test
  141. @UseDataProvider("booleanValues")
  142. public void organization_is_null(boolean allStepsExecuted) {
  143. underTest.finished(allStepsExecuted);
  144. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  145. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getOrganization()).isEmpty();
  146. }
  147. @Test
  148. @UseDataProvider("booleanValues")
  149. public void CeTask_status_depends_on_finished_method_argument_is_true_or_false(boolean allStepsExecuted) {
  150. underTest.finished(allStepsExecuted);
  151. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  152. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getCeTask().getStatus())
  153. .isEqualTo(
  154. allStepsExecuted ? org.sonar.api.ce.posttask.CeTask.Status.SUCCESS : org.sonar.api.ce.posttask.CeTask.Status.FAILED);
  155. }
  156. @Test
  157. public void ceTask_uuid_is_UUID_of_CeTask() {
  158. underTest.finished(true);
  159. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  160. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getCeTask().getId())
  161. .isEqualTo(ceTask.getUuid());
  162. }
  163. @Test
  164. public void project_uuid_key_and_name_come_from_CeTask() {
  165. underTest.finished(true);
  166. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  167. Project project = taskContextCaptor.getValue().getProjectAnalysis().getProject();
  168. assertThat(project.getUuid()).isEqualTo(ceTask.getEntity().get().getUuid());
  169. assertThat(project.getKey()).isEqualTo(ceTask.getEntity().get().getKey().get());
  170. assertThat(project.getName()).isEqualTo(ceTask.getEntity().get().getName().get());
  171. }
  172. @Test
  173. public void date_comes_from_AnalysisMetadataHolder() {
  174. analysisMetadataHolder.setAnalysisDate(8_465_132_498L);
  175. analysisMetadataHolder.setUuid(RandomStringUtils.randomAlphanumeric(40));
  176. underTest.finished(true);
  177. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  178. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis().get().getDate())
  179. .isEqualTo(new Date(analysisMetadataHolder.getAnalysisDate()));
  180. }
  181. @Test
  182. public void analysisDate_and_analysisUuid_comes_from_AnalysisMetadataHolder_when_set() {
  183. analysisMetadataHolder.setAnalysisDate(8465132498L);
  184. analysisMetadataHolder.setUuid(RandomStringUtils.randomAlphanumeric(40));
  185. underTest.finished(true);
  186. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  187. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis().get().getDate())
  188. .isEqualTo(new Date(analysisMetadataHolder.getAnalysisDate()));
  189. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis().get().getAnalysisUuid())
  190. .isEqualTo(analysisMetadataHolder.getUuid());
  191. }
  192. @Test
  193. public void analysis_is_empty_when_not_set_in_AnalysisMetadataHolder() {
  194. underTest.finished(false);
  195. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  196. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis()).isEmpty();
  197. }
  198. @Test
  199. public void branch_comes_from_AnalysisMetadataHolder_when_set() {
  200. analysisMetadataHolder.setBranch(new Branch() {
  201. @Override
  202. public BranchType getType() {
  203. return BranchType.BRANCH;
  204. }
  205. @Override
  206. public boolean isMain() {
  207. return false;
  208. }
  209. @Override
  210. public String getReferenceBranchUuid() {
  211. throw new UnsupportedOperationException();
  212. }
  213. @Override
  214. public String getName() {
  215. return "feature/foo";
  216. }
  217. @Override
  218. public boolean supportsCrossProjectCpd() {
  219. throw new UnsupportedOperationException();
  220. }
  221. @Override
  222. public String getPullRequestKey() {
  223. throw new UnsupportedOperationException();
  224. }
  225. @Override
  226. public String getTargetBranchName() {
  227. throw new UnsupportedOperationException();
  228. }
  229. @Override
  230. public String generateKey(String projectKey, @Nullable String fileOrDirPath) {
  231. throw new UnsupportedOperationException();
  232. }
  233. });
  234. underTest.finished(true);
  235. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  236. org.sonar.api.ce.posttask.Branch branch = taskContextCaptor.getValue().getProjectAnalysis().getBranch().get();
  237. assertThat(branch.isMain()).isFalse();
  238. assertThat(branch.getName()).hasValue("feature/foo");
  239. assertThat(branch.getType()).isEqualTo(BranchImpl.Type.BRANCH);
  240. }
  241. @Test
  242. public void qualityGate_is_null_when_finished_method_argument_is_false() {
  243. underTest.finished(false);
  244. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  245. assertThat(taskContextCaptor.getValue().getProjectAnalysis().getQualityGate()).isNull();
  246. }
  247. @Test
  248. public void qualityGate_is_populated_when_finished_method_argument_is_true() {
  249. underTest.finished(true);
  250. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  251. org.sonar.api.ce.posttask.QualityGate qualityGate = taskContextCaptor.getValue().getProjectAnalysis().getQualityGate();
  252. assertThat(qualityGate.getStatus()).isEqualTo(org.sonar.api.ce.posttask.QualityGate.Status.OK);
  253. assertThat(qualityGate.getId()).isEqualTo(QUALITY_GATE_UUID);
  254. assertThat(qualityGate.getName()).isEqualTo(QUALITY_GATE_NAME);
  255. assertThat(qualityGate.getConditions()).hasSize(2);
  256. }
  257. @Test
  258. public void scannerContext_loads_properties_from_scanner_report() {
  259. reportReader.putContextProperties(asList(ScannerReport.ContextProperty.newBuilder().setKey("foo").setValue("bar").build()));
  260. underTest.finished(true);
  261. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  262. org.sonar.api.ce.posttask.ScannerContext scannerContext = taskContextCaptor.getValue().getProjectAnalysis().getScannerContext();
  263. assertThat(scannerContext.getProperties()).containsExactly(entry("foo", "bar"));
  264. }
  265. @Test
  266. @UseDataProvider("booleanValues")
  267. public void logStatistics_add_fails_when_NPE_if_key_or_value_is_null(boolean allStepsExecuted) {
  268. underTest.finished(allStepsExecuted);
  269. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  270. PostProjectAnalysisTask.LogStatistics logStatistics = taskContextCaptor.getValue().getLogStatistics();
  271. assertThat(catchThrowable(() -> logStatistics.add(null, "foo")))
  272. .isInstanceOf(NullPointerException.class)
  273. .hasMessage("Statistic has null key");
  274. assertThat(catchThrowable(() -> logStatistics.add(null, null)))
  275. .isInstanceOf(NullPointerException.class)
  276. .hasMessage("Statistic has null key");
  277. assertThat(catchThrowable(() -> logStatistics.add("bar", null)))
  278. .isInstanceOf(NullPointerException.class)
  279. .hasMessage("Statistic with key [bar] has null value");
  280. }
  281. @Test
  282. @UseDataProvider("booleanValues")
  283. public void logStatistics_add_fails_with_IAE_if_key_is_time_or_status_ignoring_case(boolean allStepsExecuted) {
  284. underTest.finished(allStepsExecuted);
  285. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  286. PostProjectAnalysisTask.LogStatistics logStatistics = taskContextCaptor.getValue().getLogStatistics();
  287. for (String reservedKey : asList("time", "TIME", "TImE", "status", "STATUS", "STaTuS")) {
  288. assertThat(catchThrowable(() -> logStatistics.add(reservedKey, "foo")))
  289. .isInstanceOf(IllegalArgumentException.class)
  290. .hasMessage("Statistic with key [" + reservedKey + "] is not accepted");
  291. }
  292. }
  293. @Test
  294. @UseDataProvider("booleanValues")
  295. public void logStatistics_add_fails_with_IAE_if_same_key_with_exact_case_added_twice(boolean allStepsExecuted) {
  296. underTest.finished(allStepsExecuted);
  297. verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
  298. PostProjectAnalysisTask.LogStatistics logStatistics = taskContextCaptor.getValue().getLogStatistics();
  299. String key = RandomStringUtils.randomAlphabetic(10);
  300. logStatistics.add(key, new Object());
  301. assertThat(catchThrowable(() -> logStatistics.add(key, "bar")))
  302. .isInstanceOf(IllegalArgumentException.class)
  303. .hasMessage("Statistic with key [" + key + "] is already present");
  304. }
  305. @Test
  306. @UseDataProvider("booleanValues")
  307. public void logStatistics_adds_statistics_to_end_of_task_log(boolean allStepsExecuted) {
  308. Map<String, Object> stats = new HashMap<>();
  309. for (int i = 0; i <= new Random().nextInt(10); i++) {
  310. stats.put("statKey_" + i, "statVal_" + i);
  311. }
  312. PostProjectAnalysisTask logStatisticsTask = mock(PostProjectAnalysisTask.class);
  313. when(logStatisticsTask.getDescription()).thenReturn("PT1");
  314. doAnswer(i -> {
  315. PostProjectAnalysisTask.Context context = i.getArgument(0);
  316. stats.forEach((k, v) -> context.getLogStatistics().add(k, v));
  317. return null;
  318. }).when(logStatisticsTask)
  319. .finished(any(PostProjectAnalysisTask.Context.class));
  320. new PostProjectAnalysisTasksExecutor(
  321. ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader, new PostProjectAnalysisTask[] {logStatisticsTask})
  322. .finished(allStepsExecuted);
  323. verify(logStatisticsTask).finished(taskContextCaptor.capture());
  324. assertThat(logTester.logs()).hasSize(1);
  325. List<String> logs = logTester.logs(Level.INFO);
  326. assertThat(logs).hasSize(1);
  327. StringBuilder expectedLog = new StringBuilder("^PT1 ");
  328. stats.forEach((k, v) -> expectedLog.append("\\| " + k + "=" + v + " "));
  329. expectedLog.append("\\| status=SUCCESS \\| time=\\d+ms$");
  330. assertThat(logs.get(0)).matches(expectedLog.toString());
  331. }
  332. @Test
  333. @UseDataProvider("booleanValues")
  334. public void finished_does_not_fail_if_listener_throws_exception_and_execute_subsequent_listeners(boolean allStepsExecuted) {
  335. PostProjectAnalysisTask postProjectAnalysisTask1 = newPostProjectAnalysisTask("PT1");
  336. PostProjectAnalysisTask postProjectAnalysisTask2 = newPostProjectAnalysisTask("PT2");
  337. PostProjectAnalysisTask postProjectAnalysisTask3 = newPostProjectAnalysisTask("PT3");
  338. InOrder inOrder = inOrder(postProjectAnalysisTask1, postProjectAnalysisTask2, postProjectAnalysisTask3);
  339. doThrow(new RuntimeException("Faking a listener throws an exception"))
  340. .when(postProjectAnalysisTask2)
  341. .finished(any(PostProjectAnalysisTask.Context.class));
  342. new PostProjectAnalysisTasksExecutor(
  343. ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader,
  344. new PostProjectAnalysisTask[] {postProjectAnalysisTask1, postProjectAnalysisTask2, postProjectAnalysisTask3})
  345. .finished(allStepsExecuted);
  346. inOrder.verify(postProjectAnalysisTask1).finished(taskContextCaptor.capture());
  347. inOrder.verify(postProjectAnalysisTask1).getDescription();
  348. inOrder.verify(postProjectAnalysisTask2).finished(taskContextCaptor.capture());
  349. inOrder.verify(postProjectAnalysisTask2).getDescription();
  350. inOrder.verify(postProjectAnalysisTask3).finished(taskContextCaptor.capture());
  351. inOrder.verify(postProjectAnalysisTask3).getDescription();
  352. inOrder.verifyNoMoreInteractions();
  353. assertThat(logTester.logs()).hasSize(4);
  354. List<String> logs = logTester.logs(Level.INFO);
  355. assertThat(logs).hasSize(3);
  356. assertThat(logs.get(0)).matches("^PT1 \\| status=SUCCESS \\| time=\\d+ms$");
  357. assertThat(logs.get(1)).matches("^PT2 \\| status=FAILED \\| time=\\d+ms$");
  358. assertThat(logs.get(2)).matches("^PT3 \\| status=SUCCESS \\| time=\\d+ms$");
  359. }
  360. @DataProvider
  361. public static Object[][] booleanValues() {
  362. return new Object[][] {
  363. {true},
  364. {false}
  365. };
  366. }
  367. private static Condition createCondition(String metricKey) {
  368. Metric metric = mock(Metric.class);
  369. when(metric.getKey()).thenReturn(metricKey);
  370. return new Condition(metric, Condition.Operator.LESS_THAN.getDbValue(), "error threshold");
  371. }
  372. private static PostProjectAnalysisTask newPostProjectAnalysisTask(String description) {
  373. PostProjectAnalysisTask res = mock(PostProjectAnalysisTask.class);
  374. when(res.getDescription()).thenReturn(description);
  375. doAnswer(i -> null).when(res).finished(any(PostProjectAnalysisTask.Context.class));
  376. return res;
  377. }
  378. private static List<PostProjectAnalysisTask.ProjectAnalysis> getAllProjectAnalyses(ArgumentCaptor<PostProjectAnalysisTask.Context> taskContextCaptor) {
  379. return taskContextCaptor.getAllValues()
  380. .stream()
  381. .map(PostProjectAnalysisTask.Context::getProjectAnalysis)
  382. .collect(toList());
  383. }
  384. }