/*
* 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: " +
"
" +
"- %s - get a specific test
" +
"- %s - get the tests in a test file
" +
"- %s - get the tests in a test file
" +
"- %s and %6$s - get the tests that cover a specific line of code
" +
"- %s and %6$s - get the tests that cover a specific line of code
" +
"
",
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();
}
}
}