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.

ListAction.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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.server.test.ws;
  21. import com.google.common.base.Function;
  22. import com.google.common.collect.Lists;
  23. import com.google.common.collect.Maps;
  24. import com.google.common.io.Resources;
  25. import java.util.List;
  26. import java.util.Map;
  27. import javax.annotation.Nonnull;
  28. import javax.annotation.Nullable;
  29. import org.apache.commons.lang.StringUtils;
  30. import org.sonar.api.server.ws.Change;
  31. import org.sonar.api.server.ws.Request;
  32. import org.sonar.api.server.ws.Response;
  33. import org.sonar.api.server.ws.WebService;
  34. import org.sonar.core.util.Uuids;
  35. import org.sonar.db.DbClient;
  36. import org.sonar.db.DbSession;
  37. import org.sonar.db.component.ComponentDto;
  38. import org.sonar.server.component.ComponentFinder;
  39. import org.sonar.server.es.SearchOptions;
  40. import org.sonar.server.es.SearchResult;
  41. import org.sonar.server.test.index.CoveredFileDoc;
  42. import org.sonar.server.test.index.TestDoc;
  43. import org.sonar.server.test.index.TestIndex;
  44. import org.sonar.server.user.UserSession;
  45. import org.sonar.server.ws.KeyExamples;
  46. import org.sonar.server.ws.WsUtils;
  47. import org.sonarqube.ws.Common;
  48. import org.sonarqube.ws.Tests;
  49. import static org.sonar.api.server.ws.WebService.Param.PAGE;
  50. import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
  51. import static org.sonar.api.web.UserRole.CODEVIEWER;
  52. import static org.sonar.core.util.Protobuf.setNullable;
  53. import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
  54. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  55. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  56. import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
  57. public class ListAction implements TestsWsAction {
  58. public static final String TEST_ID = "testId";
  59. public static final String TEST_FILE_ID = "testFileId";
  60. public static final String TEST_FILE_KEY = "testFileKey";
  61. public static final String SOURCE_FILE_ID = "sourceFileId";
  62. public static final String SOURCE_FILE_KEY = "sourceFileKey";
  63. public static final String SOURCE_FILE_LINE_NUMBER = "sourceFileLineNumber";
  64. public static final String PARAM_BRANCH = "branch";
  65. public static final String PARAM_PULL_REQUEST = "pullRequest";
  66. private final DbClient dbClient;
  67. private final TestIndex testIndex;
  68. private final UserSession userSession;
  69. private final ComponentFinder componentFinder;
  70. public ListAction(DbClient dbClient, TestIndex testIndex, UserSession userSession, ComponentFinder componentFinder) {
  71. this.dbClient = dbClient;
  72. this.testIndex = testIndex;
  73. this.userSession = userSession;
  74. this.componentFinder = componentFinder;
  75. }
  76. @Override
  77. public void define(WebService.NewController controller) {
  78. WebService.NewAction action = controller
  79. .createAction("list")
  80. .setDescription(String.format(
  81. "Get the list of tests either in a test file or that test a given line of source code.<br /> " +
  82. "Requires 'Browse' permission on the file's project.<br /> " +
  83. "One (and only one) of the following combination of parameters must be provided: " +
  84. "<ul>" +
  85. "<li>%s - get a specific test</li>" +
  86. "<li>%s - get the tests in a test file</li>" +
  87. "<li>%s - get the tests in a test file</li>" +
  88. "<li>%s and %6$s - get the tests that cover a specific line of code</li>" +
  89. "<li>%s and %6$s - get the tests that cover a specific line of code</li>" +
  90. "</ul>",
  91. TEST_ID, TEST_FILE_ID, TEST_FILE_KEY, SOURCE_FILE_ID, SOURCE_FILE_KEY, SOURCE_FILE_LINE_NUMBER))
  92. .setSince("5.2")
  93. .setResponseExample(Resources.getResource(getClass(), "tests-example-list.json"))
  94. .setDeprecatedSince("5.6")
  95. .setHandler(this)
  96. .setChangelog(new Change("6.6", "\"fileBranch\" field is now returned"))
  97. .setChangelog(new Change("7.1", "\"filePullRequest\" field is now returned"))
  98. .addPagingParams(100, MAX_LIMIT);
  99. action
  100. .createParam(TEST_FILE_ID)
  101. .setDescription("ID of test file")
  102. .setExampleValue(Uuids.UUID_EXAMPLE_01);
  103. action
  104. .createParam(TEST_FILE_KEY)
  105. .setDescription("Key of test file")
  106. .setExampleValue("MY_PROJECT:src/test/java/foo/BarTest.java");
  107. action
  108. .createParam(TEST_ID)
  109. .setDescription("ID of test")
  110. .setExampleValue(Uuids.UUID_EXAMPLE_02);
  111. action
  112. .createParam(SOURCE_FILE_ID)
  113. .setDescription("ID of source file. Must be provided with the source file line number.")
  114. .setExampleValue(Uuids.UUID_EXAMPLE_03);
  115. action
  116. .createParam(SOURCE_FILE_KEY)
  117. .setSince("5.4")
  118. .setDescription("Key of source file. Must be provided with the source file line number.")
  119. .setExampleValue(KeyExamples.KEY_FILE_EXAMPLE_001);
  120. action
  121. .createParam(SOURCE_FILE_LINE_NUMBER)
  122. .setDescription("Source file line number. Must be provided with the source file ID or key.")
  123. .setExampleValue("10");
  124. action.createParam(PARAM_BRANCH)
  125. .setDescription("Branch key")
  126. .setSince("6.6")
  127. .setInternal(true)
  128. .setExampleValue(KEY_BRANCH_EXAMPLE_001);
  129. action.createParam(PARAM_PULL_REQUEST)
  130. .setDescription("Pull request id")
  131. .setSince("7.1")
  132. .setInternal(true)
  133. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
  134. }
  135. @Override
  136. public void handle(Request request, Response response) throws Exception {
  137. String testUuid = request.param(TEST_ID);
  138. String testFileUuid = request.param(TEST_FILE_ID);
  139. String testFileKey = request.param(TEST_FILE_KEY);
  140. String sourceFileUuid = request.param(SOURCE_FILE_ID);
  141. String sourceFileKey = request.param(SOURCE_FILE_KEY);
  142. String branch = request.param(PARAM_BRANCH);
  143. String pullRequest = request.param(PARAM_PULL_REQUEST);
  144. Integer sourceFileLineNumber = request.paramAsInt(SOURCE_FILE_LINE_NUMBER);
  145. SearchOptions searchOptions = new SearchOptions().setPage(
  146. request.mandatoryParamAsInt(PAGE),
  147. request.mandatoryParamAsInt(PAGE_SIZE));
  148. SearchResult<TestDoc> tests;
  149. Map<String, ComponentDto> componentsByTestFileUuid;
  150. try (DbSession dbSession = dbClient.openSession(false)) {
  151. tests = searchTests(dbSession, testUuid, testFileUuid, testFileKey, sourceFileUuid, sourceFileKey, branch, pullRequest, sourceFileLineNumber, searchOptions);
  152. componentsByTestFileUuid = buildComponentsByTestFileUuid(dbSession, tests.getDocs());
  153. }
  154. Tests.ListResponse.Builder responseBuilder = Tests.ListResponse.newBuilder();
  155. responseBuilder.setPaging(Common.Paging.newBuilder()
  156. .setPageIndex(searchOptions.getPage())
  157. .setPageSize(searchOptions.getLimit())
  158. .setTotal((int) tests.getTotal())
  159. .build());
  160. for (TestDoc testDoc : tests.getDocs()) {
  161. Tests.Test.Builder testBuilder = Tests.Test.newBuilder();
  162. testBuilder.setId(testDoc.testUuid());
  163. testBuilder.setName(StringUtils.defaultString(testDoc.name()));
  164. testBuilder.setFileId(testDoc.fileUuid());
  165. ComponentDto component = componentsByTestFileUuid.get(testDoc.fileUuid());
  166. if (component != null) {
  167. testBuilder.setFileKey(component.getKey());
  168. testBuilder.setFileName(component.longName());
  169. setNullable(component.getBranch(), testBuilder::setFileBranch);
  170. setNullable(component.getPullRequest(), testBuilder::setFilePullRequest);
  171. }
  172. testBuilder.setStatus(Tests.TestStatus.valueOf(testDoc.status()));
  173. if (testDoc.durationInMs() != null) {
  174. testBuilder.setDurationInMs(testDoc.durationInMs());
  175. }
  176. testBuilder.setCoveredLines(coveredLines(testDoc.coveredFiles()));
  177. if (testDoc.message() != null) {
  178. testBuilder.setMessage(testDoc.message());
  179. }
  180. if (testDoc.stackTrace() != null) {
  181. testBuilder.setStacktrace(testDoc.stackTrace());
  182. }
  183. responseBuilder.addTests(testBuilder.build());
  184. }
  185. WsUtils.writeProtobuf(responseBuilder.build(), request, response);
  186. }
  187. private static int coveredLines(List<CoveredFileDoc> coveredFiles) {
  188. int numberOfLinesCovered = 0;
  189. for (CoveredFileDoc coveredFile : coveredFiles) {
  190. numberOfLinesCovered += coveredFile.coveredLines().size();
  191. }
  192. return numberOfLinesCovered;
  193. }
  194. private Map<String, ComponentDto> buildComponentsByTestFileUuid(DbSession dbSession, List<TestDoc> tests) {
  195. List<String> fileUuids = Lists.transform(tests, new TestToFileUuidFunction());
  196. List<ComponentDto> components = dbClient.componentDao().selectByUuids(dbSession, fileUuids);
  197. return Maps.uniqueIndex(components, ComponentDto::uuid);
  198. }
  199. private SearchResult<TestDoc> searchTests(DbSession dbSession, @Nullable String testUuid, @Nullable String testFileUuid, @Nullable String testFileKey,
  200. @Nullable String sourceFileUuid, @Nullable String sourceFileKey, @Nullable String branch, @Nullable String pullRequest,
  201. @Nullable Integer sourceFileLineNumber, SearchOptions searchOptions) {
  202. if (testUuid != null) {
  203. TestDoc testDoc = checkFoundWithOptional(testIndex.getNullableByTestUuid(testUuid), "Test with id '%s' is not found", testUuid);
  204. checkComponentUuidPermission(dbSession, testDoc.fileUuid());
  205. return testIndex.searchByTestUuid(testUuid, searchOptions);
  206. }
  207. if (testFileUuid != null) {
  208. checkComponentUuidPermission(dbSession, testFileUuid);
  209. return testIndex.searchByTestFileUuid(testFileUuid, searchOptions);
  210. }
  211. if (testFileKey != null) {
  212. ComponentDto testFile = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, testFileKey, branch, pullRequest);
  213. userSession.checkComponentPermission(CODEVIEWER, testFile);
  214. return testIndex.searchByTestFileUuid(testFile.uuid(), searchOptions);
  215. }
  216. if (sourceFileUuid != null && sourceFileLineNumber != null) {
  217. ComponentDto sourceFile = componentFinder.getByUuid(dbSession, sourceFileUuid);
  218. userSession.checkComponentPermission(CODEVIEWER, sourceFile);
  219. return testIndex.searchBySourceFileUuidAndLineNumber(sourceFile.uuid(), sourceFileLineNumber, searchOptions);
  220. }
  221. if (sourceFileKey != null && sourceFileLineNumber != null) {
  222. ComponentDto sourceFile = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, sourceFileKey, branch, pullRequest);
  223. userSession.checkComponentPermission(CODEVIEWER, sourceFile);
  224. return testIndex.searchBySourceFileUuidAndLineNumber(sourceFile.uuid(), sourceFileLineNumber, searchOptions);
  225. }
  226. throw new IllegalArgumentException(
  227. "One (and only one) of the following combination of parameters must be provided: 1) test UUID. 2) test file UUID. " +
  228. "3) test file key. 4) source file ID or key with a source file line number.");
  229. }
  230. private void checkComponentUuidPermission(DbSession dbSession, String componentUuid) {
  231. ComponentDto component = componentFinder.getByUuid(dbSession, componentUuid);
  232. userSession.checkComponentPermission(CODEVIEWER, component);
  233. }
  234. private static class TestToFileUuidFunction implements Function<TestDoc, String> {
  235. @Override
  236. public String apply(@Nonnull TestDoc testDoc) {
  237. return testDoc.fileUuid();
  238. }
  239. }
  240. }