/* * SonarQube * Copyright (C) 2009-2019 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.server.test.ws; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.Resources; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.core.util.Uuids; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.es.SearchOptions; import org.sonar.server.es.SearchResult; import org.sonar.server.test.index.CoveredFileDoc; import org.sonar.server.test.index.TestDoc; import org.sonar.server.test.index.TestIndex; import org.sonar.server.user.UserSession; import org.sonar.server.ws.KeyExamples; import org.sonar.server.ws.WsUtils; import org.sonarqube.ws.Common; import org.sonarqube.ws.Tests; import static org.sonar.api.server.ws.WebService.Param.PAGE; import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; import static org.sonar.api.web.UserRole.CODEVIEWER; import static org.sonar.core.util.Protobuf.setNullable; import static org.sonar.server.es.SearchOptions.MAX_LIMIT; import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001; import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; public class ListAction implements TestsWsAction { public static final String TEST_ID = "testId"; public static final String TEST_FILE_ID = "testFileId"; public static final String TEST_FILE_KEY = "testFileKey"; public static final String SOURCE_FILE_ID = "sourceFileId"; public static final String SOURCE_FILE_KEY = "sourceFileKey"; public static final String SOURCE_FILE_LINE_NUMBER = "sourceFileLineNumber"; public static final String PARAM_BRANCH = "branch"; public static final String PARAM_PULL_REQUEST = "pullRequest"; private final DbClient dbClient; private final TestIndex testIndex; private final UserSession userSession; private final ComponentFinder componentFinder; public ListAction(DbClient dbClient, TestIndex testIndex, UserSession userSession, ComponentFinder componentFinder) { this.dbClient = dbClient; this.testIndex = testIndex; this.userSession = userSession; this.componentFinder = componentFinder; } @Override public void define(WebService.NewController controller) { WebService.NewAction action = controller .createAction("list") .setDescription(String.format( "Get the list of tests either in a test file or that test a given line of source code.
" + "Requires 'Browse' permission on the file's project.
" + "One (and only one) of the following combination of parameters must be provided: " + "", TEST_ID, TEST_FILE_ID, TEST_FILE_KEY, SOURCE_FILE_ID, SOURCE_FILE_KEY, SOURCE_FILE_LINE_NUMBER)) .setSince("5.2") .setResponseExample(Resources.getResource(getClass(), "tests-example-list.json")) .setDeprecatedSince("5.6") .setHandler(this) .setChangelog(new Change("6.6", "\"fileBranch\" field is now returned")) .setChangelog(new Change("7.1", "\"filePullRequest\" field is now returned")) .addPagingParams(100, MAX_LIMIT); action .createParam(TEST_FILE_ID) .setDescription("ID of test file") .setExampleValue(Uuids.UUID_EXAMPLE_01); action .createParam(TEST_FILE_KEY) .setDescription("Key of test file") .setExampleValue("MY_PROJECT:src/test/java/foo/BarTest.java"); action .createParam(TEST_ID) .setDescription("ID of test") .setExampleValue(Uuids.UUID_EXAMPLE_02); action .createParam(SOURCE_FILE_ID) .setDescription("ID of source file. Must be provided with the source file line number.") .setExampleValue(Uuids.UUID_EXAMPLE_03); action .createParam(SOURCE_FILE_KEY) .setSince("5.4") .setDescription("Key of source file. Must be provided with the source file line number.") .setExampleValue(KeyExamples.KEY_FILE_EXAMPLE_001); action .createParam(SOURCE_FILE_LINE_NUMBER) .setDescription("Source file line number. Must be provided with the source file ID or key.") .setExampleValue("10"); action.createParam(PARAM_BRANCH) .setDescription("Branch key") .setSince("6.6") .setInternal(true) .setExampleValue(KEY_BRANCH_EXAMPLE_001); action.createParam(PARAM_PULL_REQUEST) .setDescription("Pull request id") .setSince("7.1") .setInternal(true) .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001); } @Override public void handle(Request request, Response response) throws Exception { String testUuid = request.param(TEST_ID); String testFileUuid = request.param(TEST_FILE_ID); String testFileKey = request.param(TEST_FILE_KEY); String sourceFileUuid = request.param(SOURCE_FILE_ID); String sourceFileKey = request.param(SOURCE_FILE_KEY); String branch = request.param(PARAM_BRANCH); String pullRequest = request.param(PARAM_PULL_REQUEST); Integer sourceFileLineNumber = request.paramAsInt(SOURCE_FILE_LINE_NUMBER); SearchOptions searchOptions = new SearchOptions().setPage( request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE)); SearchResult tests; Map componentsByTestFileUuid; try (DbSession dbSession = dbClient.openSession(false)) { tests = searchTests(dbSession, testUuid, testFileUuid, testFileKey, sourceFileUuid, sourceFileKey, branch, pullRequest, sourceFileLineNumber, searchOptions); componentsByTestFileUuid = buildComponentsByTestFileUuid(dbSession, tests.getDocs()); } Tests.ListResponse.Builder responseBuilder = Tests.ListResponse.newBuilder(); responseBuilder.setPaging(Common.Paging.newBuilder() .setPageIndex(searchOptions.getPage()) .setPageSize(searchOptions.getLimit()) .setTotal((int) tests.getTotal()) .build()); for (TestDoc testDoc : tests.getDocs()) { Tests.Test.Builder testBuilder = Tests.Test.newBuilder(); testBuilder.setId(testDoc.testUuid()); testBuilder.setName(StringUtils.defaultString(testDoc.name())); testBuilder.setFileId(testDoc.fileUuid()); ComponentDto component = componentsByTestFileUuid.get(testDoc.fileUuid()); if (component != null) { testBuilder.setFileKey(component.getKey()); testBuilder.setFileName(component.longName()); setNullable(component.getBranch(), testBuilder::setFileBranch); setNullable(component.getPullRequest(), testBuilder::setFilePullRequest); } testBuilder.setStatus(Tests.TestStatus.valueOf(testDoc.status())); if (testDoc.durationInMs() != null) { testBuilder.setDurationInMs(testDoc.durationInMs()); } testBuilder.setCoveredLines(coveredLines(testDoc.coveredFiles())); if (testDoc.message() != null) { testBuilder.setMessage(testDoc.message()); } if (testDoc.stackTrace() != null) { testBuilder.setStacktrace(testDoc.stackTrace()); } responseBuilder.addTests(testBuilder.build()); } WsUtils.writeProtobuf(responseBuilder.build(), request, response); } private static int coveredLines(List coveredFiles) { int numberOfLinesCovered = 0; for (CoveredFileDoc coveredFile : coveredFiles) { numberOfLinesCovered += coveredFile.coveredLines().size(); } return numberOfLinesCovered; } private Map buildComponentsByTestFileUuid(DbSession dbSession, List tests) { List fileUuids = Lists.transform(tests, new TestToFileUuidFunction()); List components = dbClient.componentDao().selectByUuids(dbSession, fileUuids); return Maps.uniqueIndex(components, ComponentDto::uuid); } private SearchResult searchTests(DbSession dbSession, @Nullable String testUuid, @Nullable String testFileUuid, @Nullable String testFileKey, @Nullable String sourceFileUuid, @Nullable String sourceFileKey, @Nullable String branch, @Nullable String pullRequest, @Nullable Integer sourceFileLineNumber, SearchOptions searchOptions) { if (testUuid != null) { TestDoc testDoc = checkFoundWithOptional(testIndex.getNullableByTestUuid(testUuid), "Test with id '%s' is not found", testUuid); checkComponentUuidPermission(dbSession, testDoc.fileUuid()); return testIndex.searchByTestUuid(testUuid, searchOptions); } if (testFileUuid != null) { checkComponentUuidPermission(dbSession, testFileUuid); return testIndex.searchByTestFileUuid(testFileUuid, searchOptions); } if (testFileKey != null) { ComponentDto testFile = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, testFileKey, branch, pullRequest); userSession.checkComponentPermission(CODEVIEWER, testFile); return testIndex.searchByTestFileUuid(testFile.uuid(), searchOptions); } if (sourceFileUuid != null && sourceFileLineNumber != null) { ComponentDto sourceFile = componentFinder.getByUuid(dbSession, sourceFileUuid); userSession.checkComponentPermission(CODEVIEWER, sourceFile); return testIndex.searchBySourceFileUuidAndLineNumber(sourceFile.uuid(), sourceFileLineNumber, searchOptions); } if (sourceFileKey != null && sourceFileLineNumber != null) { ComponentDto sourceFile = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, sourceFileKey, branch, pullRequest); userSession.checkComponentPermission(CODEVIEWER, sourceFile); return testIndex.searchBySourceFileUuidAndLineNumber(sourceFile.uuid(), sourceFileLineNumber, searchOptions); } throw new IllegalArgumentException( "One (and only one) of the following combination of parameters must be provided: 1) test UUID. 2) test file UUID. " + "3) test file key. 4) source file ID or key with a source file line number."); } private void checkComponentUuidPermission(DbSession dbSession, String componentUuid) { ComponentDto component = componentFinder.getByUuid(dbSession, componentUuid); userSession.checkComponentPermission(CODEVIEWER, component); } private static class TestToFileUuidFunction implements Function { @Override public String apply(@Nonnull TestDoc testDoc) { return testDoc.fileUuid(); } } }