From: Teryk Bellahsene Date: Tue, 24 Jan 2017 13:55:29 +0000 (+0100) Subject: SONAR-7305 Create WS api/measures/search_history X-Git-Tag: 6.3-RC1~437 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=f55c90fa06e82cd70639eda3c2a6f686a13fbee7;p=sonarqube.git SONAR-7305 Create WS api/measures/search_history --- diff --git a/it/it-tests/src/test/java/it/dbCleaner/PurgeTest.java b/it/it-tests/src/test/java/it/dbCleaner/PurgeTest.java index 73501238752..1095e543667 100644 --- a/it/it-tests/src/test/java/it/dbCleaner/PurgeTest.java +++ b/it/it-tests/src/test/java/it/dbCleaner/PurgeTest.java @@ -35,10 +35,12 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; -import org.sonarqube.ws.client.GetRequest; -import org.sonarqube.ws.client.WsResponse; +import org.sonarqube.ws.WsMeasures; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryValue; +import org.sonarqube.ws.client.measure.SearchHistoryRequest; import util.ItUtils; +import static java.util.Collections.singletonList; import static org.apache.commons.lang.time.DateUtils.addDays; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -205,14 +207,12 @@ public class PurgeTest { runProjectAnalysis(orchestrator, PROJECT_SAMPLE_PATH); // Check that only analysis from last thursday is kept (as it's the last one from previous week) - WsResponse response = newAdminWsClient(orchestrator).wsConnector().call( - new GetRequest("/api/timemachine/index") - .setParam("resource", PROJECT_KEY) - .setParam("metrics", "ncloc")) - .failIfNotSuccessful(); - String content = response.content(); - assertThat(content).contains(lastThursdayFormatted); - assertThat(content).doesNotContain(lastWednesdayFormatted); + WsMeasures.SearchHistoryResponse response = newAdminWsClient(orchestrator).measures().searchHistory(SearchHistoryRequest.builder() + .setComponent(PROJECT_KEY) + .setMetrics(singletonList("ncloc")) + .build()); + assertThat(response.getMeasuresCount()).isEqualTo(1); + assertThat(response.getMeasuresList().get(0).getHistoryList()).extracting(HistoryValue::getDate).doesNotContain(lastWednesdayFormatted, lastThursdayFormatted); } /** @@ -279,7 +279,7 @@ public class PurgeTest { scan(PROJECT_SAMPLE_PATH, ONE_DAY_AGO); // second analysis as NEW_* metrics - assertThat(count(COUNT_FILE_MEASURES)).isLessThan( 2 * fileMeasures); + assertThat(count(COUNT_FILE_MEASURES)).isLessThan(2 * fileMeasures); assertThat(count(COUNT_DIR_MEASURES)).isGreaterThan(2 * dirMeasures); } diff --git a/it/it-tests/src/test/java/it/measureHistory/TimeMachineTest.java b/it/it-tests/src/test/java/it/measureHistory/TimeMachineTest.java index 8e080218880..7c1434c68a2 100644 --- a/it/it-tests/src/test/java/it/measureHistory/TimeMachineTest.java +++ b/it/it-tests/src/test/java/it/measureHistory/TimeMachineTest.java @@ -24,33 +24,40 @@ import com.sonar.orchestrator.build.BuildResult; import com.sonar.orchestrator.build.SonarScanner; import com.sonar.orchestrator.locator.FileLocation; import it.Category1Suite; +import java.util.Arrays; import java.util.Date; import java.util.Map; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; -import org.sonar.wsclient.services.TimeMachine; -import org.sonar.wsclient.services.TimeMachineCell; -import org.sonar.wsclient.services.TimeMachineQuery; import org.sonarqube.ws.WsMeasures.Measure; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryValue; +import org.sonarqube.ws.client.measure.MeasuresService; +import org.sonarqube.ws.client.measure.SearchHistoryRequest; import util.ItUtils; import util.ItUtils.ComponentNavigation; -import static java.lang.Double.parseDouble; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.formatDate; import static util.ItUtils.getComponentNavigation; import static util.ItUtils.getMeasuresByMetricKey; import static util.ItUtils.getMeasuresWithVariationsByMetricKey; +import static util.ItUtils.newAdminWsClient; import static util.ItUtils.projectDir; import static util.ItUtils.setServerProperty; public class TimeMachineTest { private static final String PROJECT = "sample"; + private static final String FIRST_ANALYSIS_DATE = "2014-10-19"; + private static final String SECOND_ANALYSIS_DATE = "2014-11-13"; @ClassRule public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR; + private static MeasuresService wsMeasures; @BeforeClass public static void initialize() { @@ -59,8 +66,10 @@ public class TimeMachineTest { orchestrator.getServer().restoreProfile(FileLocation.ofClasspath("/measureHistory/one-issue-per-line-profile.xml")); orchestrator.getServer().provisionProject("sample", "Sample"); orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line"); - analyzeProject("measure/xoo-history-v1", "2014-10-19"); - analyzeProject("measure/xoo-history-v2", "2014-11-13"); + analyzeProject("measure/xoo-history-v1", FIRST_ANALYSIS_DATE); + analyzeProject("measure/xoo-history-v2", SECOND_ANALYSIS_DATE); + + wsMeasures = newAdminWsClient(orchestrator).measures(); } public static void initPeriods() { @@ -87,73 +96,38 @@ public class TimeMachineTest { @Test public void testHistoryOfIssues() { - TimeMachineQuery query = TimeMachineQuery.createForMetrics(PROJECT, "blocker_violations", "critical_violations", "major_violations", - "minor_violations", "info_violations"); - TimeMachine timemachine = orchestrator.getServer().getWsClient().find(query); - assertThat(timemachine.getCells().length).isEqualTo(2); - - TimeMachineCell cell1 = timemachine.getCells()[0]; - TimeMachineCell cell2 = timemachine.getCells()[1]; - - assertThat(cell1.getDate().getMonth()).isEqualTo(9); - assertThat(cell1.getValues()).isEqualTo(new Object[] {0L, 0L, 0L, 26L, 0L}); - - assertThat(cell2.getDate().getMonth()).isEqualTo(10); - assertThat(cell2.getValues()).isEqualTo(new Object[] {0L, 0L, 0L, 43L, 0L}); + SearchHistoryResponse response = searchHistory("blocker_violations", "critical_violations", "info_violations", "major_violations", "minor_violations"); + assertThat(response.getPaging().getTotal()).isEqualTo(2); + + assertHistory(response, "blocker_violations", "0", "0"); + assertHistory(response, "critical_violations", "0", "0"); + assertHistory(response, "info_violations", "0", "0"); + assertHistory(response, "major_violations", "0", "0"); + assertHistory(response, "minor_violations", "26", "43"); } @Test public void testHistoryOfMeasures() { - TimeMachineQuery query = TimeMachineQuery.createForMetrics(PROJECT, "lines", "ncloc"); - TimeMachine timemachine = orchestrator.getServer().getWsClient().find(query); - assertThat(timemachine.getCells().length).isEqualTo(2); - - TimeMachineCell cell1 = timemachine.getCells()[0]; - TimeMachineCell cell2 = timemachine.getCells()[1]; - - assertThat(cell1.getDate().getMonth()).isEqualTo(9); - assertThat(cell1.getValues()).isEqualTo(new Object[] {26L, 24L}); - - assertThat(cell2.getDate().getMonth()).isEqualTo(10); - assertThat(cell2.getValues()).isEqualTo(new Object[] {43L, 40L}); - } - - @Test - public void unknownMetrics() { - TimeMachine timemachine = orchestrator.getServer().getWsClient().find(TimeMachineQuery.createForMetrics(PROJECT, "notfound")); - assertThat(timemachine.getCells().length).isEqualTo(0); - - timemachine = orchestrator.getServer().getWsClient().find(TimeMachineQuery.createForMetrics(PROJECT, "lines", "notfound")); - assertThat(timemachine.getCells().length).isEqualTo(2); - for (TimeMachineCell cell : timemachine.getCells()) { - assertThat(cell.getValues().length).isEqualTo(1); - assertThat(cell.getValues()[0]).isInstanceOf(Long.class); - } + SearchHistoryResponse response = searchHistory("lines", "ncloc"); - timemachine = orchestrator.getServer().getWsClient().find(TimeMachineQuery.createForMetrics(PROJECT)); - assertThat(timemachine.getCells().length).isEqualTo(0); + assertThat(response.getPaging().getTotal()).isEqualTo(2); + assertHistory(response, "lines", "26", "43"); + assertHistory(response, "ncloc", "24", "40"); } @Test public void noDataForInterval() { Date now = new Date(); - TimeMachine timemachine = orchestrator.getServer().getWsClient().find(TimeMachineQuery.createForMetrics(PROJECT, "lines").setFrom(now).setTo(now)); - assertThat(timemachine.getCells().length).isEqualTo(0); - } - @Test - public void unknownResource() { - TimeMachine timemachine = orchestrator.getServer().getWsClient().find(TimeMachineQuery.createForMetrics("notfound:notfound", "lines")); - assertThat(timemachine).isNull(); - } + SearchHistoryResponse response = wsMeasures.searchHistory(SearchHistoryRequest.builder() + .setComponent(PROJECT) + .setMetrics(singletonList("lines")) + .setFrom(formatDate(now)) + .setTo(formatDate(now)) + .build()); - @Test - public void test_measure_variations() { - Map measures = getMeasuresWithVariationsByMetricKey(orchestrator, PROJECT, "files", "ncloc", "violations"); - // variations from previous analysis - assertThat(parseDouble(measures.get("files").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(1.0); - assertThat(parseDouble(measures.get("ncloc").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(16.0); - assertThat(parseDouble(measures.get("violations").getPeriods().getPeriodsValue(0).getValue())).isGreaterThan(0.0); + assertThat(response.getPaging().getTotal()).isEqualTo(0); + assertThat(response.getMeasures(0).getHistoryList()).isEmpty(); } /** @@ -164,9 +138,29 @@ public class TimeMachineTest { Map measures = getMeasuresWithVariationsByMetricKey(orchestrator, PROJECT, "violations", "new_violations"); assertThat(measures.get("violations")).isNotNull(); assertThat(measures.get("new_violations")).isNotNull(); + SearchHistoryResponse response = searchHistory("new_violations"); + assertThat(response.getMeasures(0).getHistoryCount()).isGreaterThan(0); measures = getMeasuresByMetricKey(orchestrator, PROJECT, "violations", "new_violations"); assertThat(measures.get("violations")).isNotNull(); assertThat(measures.get("new_violations")).isNull(); } + + private static SearchHistoryResponse searchHistory(String... metrics) { + return wsMeasures.searchHistory(SearchHistoryRequest.builder() + .setComponent(PROJECT) + .setMetrics(Arrays.asList(metrics)) + .build()); + } + + private static void assertHistory(SearchHistoryResponse response, String metric, String... expectedMeasures) { + for (SearchHistoryResponse.HistoryMeasure measures : response.getMeasuresList()) { + if (metric.equals(measures.getMetric())) { + assertThat(measures.getHistoryList()).extracting(HistoryValue::getValue).containsExactly(expectedMeasures); + return; + } + } + + throw new IllegalArgumentException("Metric not found"); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java index eeac683913a..8cf0f5438bd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasuresWsModule.java @@ -30,6 +30,7 @@ public class MeasuresWsModule extends Module { MeasuresWs.class, ComponentTreeAction.class, ComponentAction.class, - SearchAction.class); + SearchAction.class, + SearchHistoryAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java index bef88a5bd8c..87f4aea7bd9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MetricDtoWithBestValue.java @@ -19,17 +19,23 @@ */ package org.sonar.server.measure.ws; +import com.google.common.collect.ImmutableSortedSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import org.sonar.api.resources.Qualifiers; +import org.sonar.db.component.ComponentDto; import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonarqube.ws.WsMeasures; class MetricDtoWithBestValue { private static final String LOWER_CASE_NEW_METRIC_PREFIX = "new_"; + private static final Set QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE = ImmutableSortedSet.of(Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE); private final MetricDto metric; private final MeasureDto bestValue; @@ -57,6 +63,10 @@ class MetricDtoWithBestValue { return bestValue; } + static Predicate isEligibleForBestValue() { + return component -> QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE.contains(component.qualifier()); + } + static class MetricDtoToMetricDtoWithBestValueFunction implements Function { private final List periodIndexes; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryAction.java new file mode 100644 index 00000000000..8400bef04ab --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryAction.java @@ -0,0 +1,179 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.measure.ws; + +import com.google.common.collect.Sets; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.web.UserRole; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.component.SnapshotQuery; +import org.sonar.db.component.SnapshotQuery.SORT_FIELD; +import org.sonar.db.component.SnapshotQuery.SORT_ORDER; +import org.sonar.db.measure.MeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; +import org.sonar.server.ws.KeyExamples; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse; +import org.sonarqube.ws.client.measure.SearchHistoryRequest; + +import static java.lang.String.format; +import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime; +import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ACTION_SEARCH_HISTORY; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_FROM; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRICS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_TO; +import static org.sonarqube.ws.client.measure.SearchHistoryRequest.DEFAULT_PAGE_SIZE; +import static org.sonarqube.ws.client.measure.SearchHistoryRequest.MAX_PAGE_SIZE; + +public class SearchHistoryAction implements MeasuresWsAction { + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + + public SearchHistoryAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_SEARCH_HISTORY) + .setDescription("Search measures history of a component.
" + + "Measures are ordered chronologically.
" + + "Pagination applies to the number of measures for each metric.") + .setResponseExample(getClass().getResource("search_history-example.json")) + .setSince("6.3") + .setHandler(this); + + action.createParam(PARAM_COMPONENT) + .setDescription("Component key") + .setRequired(true) + .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001); + + action.createParam(PARAM_METRICS) + .setDescription("Comma-separated list of metric keys") + .setRequired(true) + .setExampleValue("ncloc,coverage,new_violations"); + + action.createParam(PARAM_FROM) + .setDescription("Filter measures created after the given date (inclusive). Format: date or datetime ISO formats") + .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)"); + + action.createParam(PARAM_TO) + .setDescription("Filter issues created before the given date (inclusive). Format: date or datetime ISO formats") + .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)"); + + action.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE); + } + + @Override + public void handle(Request request, Response response) throws Exception { + SearchHistoryResponse searchHistoryResponse = Stream.of(request) + .map(SearchHistoryAction::toWsRequest) + .map(search()) + .map(result -> new SearchHistoryResponseFactory(result).apply()) + .collect(Collectors.toOneElement()); + + writeProtobuf(searchHistoryResponse, request, response); + } + + private Function search() { + return request -> { + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto component = searchComponent(request, dbSession); + + SearchHistoryResult result = new SearchHistoryResult(request) + .setComponent(component) + .setAnalyses(searchAnalyses(dbSession, request, component)) + .setMetrics(searchMetrics(dbSession, request)); + return result.setMeasures(searchMeasures(dbSession, component, result.getAnalyses(), result.getMetrics())); + } + }; + } + + private ComponentDto searchComponent(SearchHistoryRequest request, DbSession dbSession) { + ComponentDto component = componentFinder.getByKey(dbSession, request.getComponent()); + userSession.checkComponentUuidPermission(UserRole.USER, component.projectUuid()); + return component; + } + + private List searchMeasures(DbSession dbSession, ComponentDto component, List analyses, List metrics) { + return dbClient.measureDao().selectPastMeasures( + dbSession, + component.uuid(), + analyses.stream().map(SnapshotDto::getUuid).collect(Collectors.toList()), + metrics.stream().map(MetricDto::getId).collect(Collectors.toList())); + } + + private List searchAnalyses(DbSession dbSession, SearchHistoryRequest request, ComponentDto component) { + SnapshotQuery dbQuery = new SnapshotQuery() + .setComponentUuid(component.projectUuid()) + .setStatus(STATUS_PROCESSED) + .setSort(SORT_FIELD.BY_DATE, SORT_ORDER.ASC); + setNullable(request.getFrom(), from -> dbQuery.setCreatedAfter(parseStartingDateOrDateTime(from).getTime())); + setNullable(request.getTo(), to -> dbQuery.setCreatedBefore(parseEndingDateOrDateTime(to).getTime() + 1_000L)); + + return dbClient.snapshotDao().selectAnalysesByQuery(dbSession, dbQuery); + } + + private List searchMetrics(DbSession dbSession, SearchHistoryRequest request) { + List metrics = dbClient.metricDao().selectByKeys(dbSession, request.getMetrics()); + if (request.getMetrics().size() > metrics.size()) { + Set requestedMetrics = request.getMetrics().stream().collect(Collectors.toSet()); + Set foundMetrics = metrics.stream().map(MetricDto::getKey).collect(Collectors.toSet()); + + Set unfoundMetrics = Sets.difference(requestedMetrics, foundMetrics).immutableCopy(); + throw new IllegalArgumentException(format("Metrics %s are not found", String.join(", ", unfoundMetrics))); + } + + return metrics; + } + + private static SearchHistoryRequest toWsRequest(Request request) { + return SearchHistoryRequest.builder() + .setComponent(request.mandatoryParam(PARAM_COMPONENT)) + .setMetrics(request.mandatoryParamAsStrings(PARAM_METRICS)) + .setFrom(request.param(PARAM_FROM)) + .setTo(request.param(PARAM_TO)) + .setPage(request.mandatoryParamAsInt(Param.PAGE)) + .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE)) + .build(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryResponseFactory.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryResponseFactory.java new file mode 100644 index 00000000000..7fd49fc81eb --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryResponseFactory.java @@ -0,0 +1,118 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.measure.ws; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import java.util.Map; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.measure.MeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryMeasure; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryValue; + +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.server.measure.ws.MeasureValueFormatter.formatMeasureValue; +import static org.sonar.server.measure.ws.MeasureValueFormatter.formatNumericalValue; + +class SearchHistoryResponseFactory { + private final SearchHistoryResult result; + private final HistoryMeasure.Builder measure; + private final HistoryValue.Builder value; + + SearchHistoryResponseFactory(SearchHistoryResult result) { + this.result = result; + this.measure = HistoryMeasure.newBuilder(); + this.value = HistoryValue.newBuilder(); + } + + public SearchHistoryResponse apply() { + return Stream.of(SearchHistoryResponse.newBuilder()) + .map(addPaging()) + .map(addMeasures()) + .map(SearchHistoryResponse.Builder::build) + .collect(Collectors.toOneElement()); + } + + private UnaryOperator addPaging() { + return response -> response.setPaging(result.getPaging()); + } + + private UnaryOperator addMeasures() { + Map metricsById = result.getMetrics().stream().collect(Collectors.uniqueIndex(MetricDto::getId)); + Map analysesByUuid = result.getAnalyses().stream().collect(Collectors.uniqueIndex(SnapshotDto::getUuid)); + Table measuresByMetricByAnalysis = HashBasedTable.create(result.getMetrics().size(), result.getAnalyses().size()); + result.getMeasures().forEach(m -> measuresByMetricByAnalysis.put(metricsById.get(m.getMetricId()), analysesByUuid.get(m.getAnalysisUuid()), m)); + + return response -> { + result.getMetrics().stream() + .peek(metric -> measure.clear()) + .map(addMetric()) + .map(metric -> addValues(measuresByMetricByAnalysis.row(metric)).apply(metric)) + .forEach(metric -> response.addMeasures(measure)); + + return response; + }; + } + + private UnaryOperator addMetric() { + return metric -> { + measure.setMetric(metric.getKey()); + return metric; + }; + } + + private UnaryOperator addValues(Map measuresByAnalysis) { + Predicate hasMeasure = analysis -> measuresByAnalysis.get(analysis) != null; + return metric -> { + result.getAnalyses().stream() + .filter(hasMeasure) + .peek(analysis -> value.clear()) + .map(addDate()) + .map(analysis -> addValue(metric, measuresByAnalysis.get(analysis)).apply(analysis)) + .forEach(analysis -> measure.addHistory(value)); + + return metric; + }; + } + + private UnaryOperator addDate() { + return analysis -> { + value.setDate(formatDateTime(analysis.getCreatedAt())); + return analysis; + }; + } + + private UnaryOperator addValue(MetricDto dbMetric, MeasureDto dbMeasure) { + return analysis -> { + String measureValue = dbMetric.getKey().startsWith("new_") + ? formatNumericalValue(dbMeasure.getVariation(1), dbMetric) + : formatMeasureValue(dbMeasure, dbMetric); + value.setValue(measureValue); + return analysis; + }; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryResult.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryResult.java new file mode 100644 index 00000000000..3d23e3c4a3b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryResult.java @@ -0,0 +1,139 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.measure.ws; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Table; +import java.util.ArrayList; +import java.util.List; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.measure.MeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.client.measure.SearchHistoryRequest; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Paging.offset; +import static org.sonar.core.util.stream.Collectors.toList; +import static org.sonar.db.metric.MetricDtoFunctions.isOptimizedForBestValue; +import static org.sonar.server.measure.ws.MetricDtoWithBestValue.isEligibleForBestValue; + +class SearchHistoryResult { + private final SearchHistoryRequest request; + private List analyses; + private List metrics; + private List measures; + private Common.Paging paging; + private ComponentDto component; + + SearchHistoryResult(SearchHistoryRequest request) { + this.request = request; + } + + boolean hasResults() { + return !analyses.isEmpty(); + } + + SearchHistoryResult setComponent(ComponentDto component) { + this.component = component; + + return this; + } + + List getAnalyses() { + return requireNonNull(analyses); + } + + SearchHistoryResult setAnalyses(List analyses) { + this.paging = Common.Paging.newBuilder().setPageIndex(request.getPage()).setPageSize(request.getPageSize()).setTotal(analyses.size()).build(); + this.analyses = analyses.stream().skip(offset(request.getPage(), request.getPageSize())).limit(request.getPageSize()).collect(toList()); + return this; + } + + List getMetrics() { + return requireNonNull(metrics); + } + + SearchHistoryResult setMetrics(List metrics) { + this.metrics = metrics; + return this; + } + + List getMeasures() { + return requireNonNull(measures); + } + + SearchHistoryResult setMeasures(List measures) { + this.measures = ImmutableList.builder() + .addAll(measures) + .addAll(addBestValuesToMeasures(component, measures)).build(); + return this; + } + + /** + * Conditions for best value measure: + *
    + *
  • component is a production file or test file
  • + *
  • metric is optimized for best value
  • + *
+ */ + private List addBestValuesToMeasures(ComponentDto component, List measures) { + if (!isEligibleForBestValue().test(component)) { + return emptyList(); + } + + requireNonNull(metrics); + requireNonNull(analyses); + + Table measuresByMetricIdAndAnalysisUuid = HashBasedTable.create(metrics.size(), analyses.size()); + measures.forEach(measure -> measuresByMetricIdAndAnalysisUuid.put(measure.getMetricId(), measure.getAnalysisUuid(), measure)); + List bestValues = new ArrayList<>(); + metrics.stream() + .filter(isOptimizedForBestValue()) + .forEach(metric -> analyses.stream() + .filter(analysis -> !measuresByMetricIdAndAnalysisUuid.contains(metric.getId(), analysis.getUuid())) + .map(analysis -> toBestValue(metric, analysis)) + .forEach(bestValues::add)); + + return bestValues; + } + + private static MeasureDto toBestValue(MetricDto metric, SnapshotDto analysis) { + MeasureDto measure = new MeasureDto() + .setMetricId(metric.getId()) + .setAnalysisUuid(analysis.getUuid()); + + if (metric.getKey().startsWith("new_")) { + measure.setVariation(1, metric.getBestValue()); + } else { + measure.setValue(metric.getBestValue()); + } + + return measure; + } + + Common.Paging getPaging() { + return requireNonNull(paging); + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search_history-example.json b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search_history-example.json new file mode 100644 index 00000000000..5e0f96fa966 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/measure/ws/search_history-example.json @@ -0,0 +1,60 @@ +{ + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 3 + }, + "measures": [ + { + "metric": "complexity", + "history": [ + { + "date": "2017-01-23T17:00:53+0100", + "value": "45" + }, + { + "date": "2017-01-24T17:00:53+0100", + "value": "45" + }, + { + "date": "2017-01-25T17:00:53+0100", + "value": "45" + } + ] + }, + { + "metric": "ncloc", + "history": [ + { + "date": "2017-01-23T17:00:53+0100", + "value": "47" + }, + { + "date": "2017-01-24T17:00:53+0100", + "value": "47" + }, + { + "date": "2017-01-25T17:00:53+0100", + "value": "47" + } + ] + }, + { + "metric": "new_violations", + "history": [ + { + "date": "2017-01-23T17:00:53+0100", + "value": "46" + }, + { + "date": "2017-01-24T17:00:53+0100", + "value": "46" + }, + { + "date": "2017-01-25T17:00:53+0100", + "value": "46" + } + ] + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java index c5a2b957d11..cfe934435bd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/MeasuresWsModuleTest.java @@ -29,6 +29,6 @@ public class MeasuresWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new MeasuresWsModule().configure(container); - assertThat(container.size()).isEqualTo(5 + 2); + assertThat(container.size()).isEqualTo(6 + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchHistoryActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchHistoryActionTest.java new file mode 100644 index 00000000000..0b6e2704b8f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchHistoryActionTest.java @@ -0,0 +1,390 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.measure.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.LongStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric.ValueType; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Common.Paging; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryMeasure; +import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryValue; +import org.sonarqube.ws.client.measure.SearchHistoryRequest; + +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.component.SnapshotDto.STATUS_UNPROCESSED; +import static org.sonar.db.component.SnapshotTesting.newAnalysis; +import static org.sonar.db.measure.MeasureTesting.newMeasureDto; +import static org.sonar.db.metric.MetricTesting.newMetricDto; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_COMPONENT; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_FROM; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRICS; +import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_TO; + +public class SearchHistoryActionTest { + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DbTester db = DbTester.create(); + private DbClient dbClient = db.getDbClient(); + private DbSession dbSession = db.getSession(); + + private WsActionTester ws = new WsActionTester(new SearchHistoryAction(dbClient, new ComponentFinder(dbClient), userSession)); + + private ComponentDto project; + private SnapshotDto analysis; + private List metrics; + private MetricDto complexityMetric; + private MetricDto nclocMetric; + private MetricDto newViolationMetric; + private SearchHistoryRequest.Builder wsRequest; + + @Before + public void setUp() { + project = newProjectDto(db.getDefaultOrganization()); + analysis = db.components().insertProjectAndSnapshot(project); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + nclocMetric = insertNclocMetric(); + complexityMetric = insertComplexityMetric(); + newViolationMetric = insertNewViolationMetric(); + metrics = newArrayList(nclocMetric.getKey(), complexityMetric.getKey(), newViolationMetric.getKey()); + wsRequest = SearchHistoryRequest.builder().setComponent(project.getKey()).setMetrics(metrics); + } + + @Test + public void empty_response() { + project = db.components().insertProject(); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + wsRequest + .setComponent(project.getKey()) + .setMetrics(singletonList(complexityMetric.getKey())); + + SearchHistoryResponse result = call(); + + assertThat(result.getMeasuresList()).hasSize(1); + assertThat(result.getMeasures(0).getHistoryCount()).isEqualTo(0); + + assertThat(result.getPaging()).extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal) + // pagination is applied to the number of analyses + .containsExactly(1, 100, 0); + } + + @Test + public void return_metrics() { + dbClient.measureDao().insert(dbSession, newMeasureDto(complexityMetric, project, analysis).setValue(42.0d)); + db.commit(); + + SearchHistoryResponse result = call(); + + assertThat(result.getMeasuresList()).hasSize(3) + .extracting(HistoryMeasure::getMetric) + .containsExactly(complexityMetric.getKey(), nclocMetric.getKey(), newViolationMetric.getKey()); + } + + @Test + public void return_measures() { + SnapshotDto laterAnalysis = dbClient.snapshotDao().insert(dbSession, newAnalysis(project).setCreatedAt(analysis.getCreatedAt() + 42_000)); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + dbClient.measureDao().insert(dbSession, + newMeasureDto(complexityMetric, project, analysis).setValue(101d), + newMeasureDto(complexityMetric, project, laterAnalysis).setValue(100d), + newMeasureDto(complexityMetric, file, analysis).setValue(42d), + newMeasureDto(nclocMetric, project, analysis).setValue(201d), + newMeasureDto(newViolationMetric, project, analysis).setVariation(1, 5d), + newMeasureDto(newViolationMetric, project, laterAnalysis).setVariation(1, 10d)); + db.commit(); + + SearchHistoryResponse result = call(); + + assertThat(result.getPaging()).extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal) + .containsExactly(1, 100, 2); + assertThat(result.getMeasuresList()).extracting(HistoryMeasure::getMetric).hasSize(3) + .containsExactly(complexityMetric.getKey(), nclocMetric.getKey(), newViolationMetric.getKey()); + String analysisDate = formatDateTime(analysis.getCreatedAt()); + String laterAnalysisDate = formatDateTime(laterAnalysis.getCreatedAt()); + // complexity measures + HistoryMeasure complexityMeasures = result.getMeasures(0); + assertThat(complexityMeasures.getMetric()).isEqualTo(complexityMetric.getKey()); + assertThat(complexityMeasures.getHistoryList()).extracting(HistoryValue::getDate, HistoryValue::getValue) + .containsExactly(tuple(analysisDate, "101"), tuple(laterAnalysisDate, "100")); + // ncloc measures + HistoryMeasure nclocMeasures = result.getMeasures(1); + assertThat(nclocMeasures.getMetric()).isEqualTo(nclocMetric.getKey()); + assertThat(nclocMeasures.getHistoryList()).extracting(HistoryValue::getDate, HistoryValue::getValue) + .containsExactly(tuple(analysisDate, "201")); + // new_violation measures + HistoryMeasure newViolationMeasures = result.getMeasures(2); + assertThat(newViolationMeasures.getMetric()).isEqualTo(newViolationMetric.getKey()); + assertThat(newViolationMeasures.getHistoryList()).extracting(HistoryValue::getDate, HistoryValue::getValue) + .containsExactly(tuple(analysisDate, "5"), tuple(laterAnalysisDate, "10")); + } + + @Test + public void pagination_applies_to_analyses() { + project = db.components().insertProject(); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + List analysisDates = LongStream.rangeClosed(1, 9) + .mapToObj(i -> dbClient.snapshotDao().insert(dbSession, newAnalysis(project).setCreatedAt(i * 1_000_000_000))) + .peek(a -> dbClient.measureDao().insert(dbSession, newMeasureDto(complexityMetric, project, a).setValue(101d))) + .map(a -> formatDateTime(a.getCreatedAt())) + .collect(Collectors.toList()); + db.commit(); + wsRequest.setComponent(project.getKey()).setPage(2).setPageSize(3); + + SearchHistoryResponse result = call(); + + assertThat(result.getPaging()).extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal).containsExactly(2, 3, 9); + assertThat(result.getMeasures(0).getHistoryList()).extracting(HistoryValue::getDate).containsExactly( + analysisDates.get(3), analysisDates.get(4), analysisDates.get(5)); + } + + @Test + public void inclusive_from_and_to_dates() { + project = db.components().insertProject(); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + List analysisDates = LongStream.rangeClosed(1, 9) + .mapToObj(i -> dbClient.snapshotDao().insert(dbSession, newAnalysis(project).setCreatedAt(System2.INSTANCE.now() + i * 1_000_000_000L))) + .peek(a -> dbClient.measureDao().insert(dbSession, newMeasureDto(complexityMetric, project, a).setValue(Double.valueOf(a.getCreatedAt())))) + .map(a -> formatDateTime(a.getCreatedAt())) + .collect(Collectors.toList()); + db.commit(); + wsRequest.setComponent(project.getKey()).setFrom(analysisDates.get(1)).setTo(analysisDates.get(3)); + + SearchHistoryResponse result = call(); + + assertThat(result.getPaging()).extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal).containsExactly(1, 100, 3); + assertThat(result.getMeasures(0).getHistoryList()).extracting(HistoryValue::getDate).containsExactly( + analysisDates.get(1), analysisDates.get(2), analysisDates.get(3)); + } + + @Test + public void return_best_values_for_files() { + dbClient.metricDao().insert(dbSession, newMetricDto().setKey("optimized").setValueType(ValueType.INT.name()).setOptimizedBestValue(true).setBestValue(456d)); + dbClient.metricDao().insert(dbSession, newMetricDto().setKey("new_optimized").setValueType(ValueType.INT.name()).setOptimizedBestValue(true).setBestValue(789d)); + db.commit(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + wsRequest.setComponent(file.getKey()).setMetrics(Arrays.asList("optimized", "new_optimized")); + + SearchHistoryResponse result = call(); + + assertThat(result.getMeasuresCount()).isEqualTo(2); + assertThat(result.getMeasuresList().get(0).getHistoryList()).extracting(HistoryValue::getValue).containsExactly("789"); + assertThat(result.getMeasuresList().get(1).getHistoryList()).extracting(HistoryValue::getValue).containsExactly("456"); + + // Best value is not applied to project + wsRequest.setComponent(project.getKey()); + result = call(); + assertThat(result.getMeasuresList().get(0).getHistoryCount()).isEqualTo(0); + } + + @Test + public void do_not_return_unprocessed_analyses() { + dbClient.snapshotDao().insert(dbSession, newAnalysis(project).setStatus(STATUS_UNPROCESSED)); + db.commit(); + + SearchHistoryResponse result = call(); + + // one analysis in setUp method + assertThat(result.getPaging().getTotal()).isEqualTo(1); + } + + @Test + public void do_not_return_developer_measures() { + wsRequest.setMetrics(singletonList(complexityMetric.getKey())); + dbClient.measureDao().insert(dbSession, newMeasureDto(complexityMetric, project, analysis).setDeveloperId(42L)); + db.commit(); + + SearchHistoryResponse result = call(); + + assertThat(result.getMeasuresCount()).isEqualTo(1); + assertThat(result.getMeasures(0).getHistoryCount()).isEqualTo(0); + } + + @Test + public void fail_if_unknown_metric() { + wsRequest.setMetrics(newArrayList(complexityMetric.getKey(), nclocMetric.getKey(), "METRIC_42", "42_METRIC")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Metrics 42_METRIC, METRIC_42 are not found"); + + call(); + } + + @Test + public void fail_if_not_enough_permissions() { + userSession.login().addProjectUuidPermissions(UserRole.ADMIN, project.uuid()); + + expectedException.expect(ForbiddenException.class); + + call(); + } + + @Test + public void fail_if_unknown_component() { + wsRequest.setComponent("PROJECT_42"); + + expectedException.expect(NotFoundException.class); + + call(); + } + + @Test + public void definition() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.key()).isEqualTo("search_history"); + assertThat(definition.responseExampleAsString()).isNotEmpty(); + assertThat(definition.isPost()).isFalse(); + assertThat(definition.isInternal()).isFalse(); + assertThat(definition.since()).isEqualTo("6.3"); + } + + @Test + public void json_example() { + project = db.components().insertProject(); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + long now = parseDateTime("2017-01-23T17:00:53+0100").getTime(); + LongStream.rangeClosed(0, 2) + .mapToObj(i -> dbClient.snapshotDao().insert(dbSession, newAnalysis(project).setCreatedAt(now + i * 24 * 1_000 * 60 * 60))) + .forEach(analysis -> dbClient.measureDao().insert(dbSession, + newMeasureDto(complexityMetric, project, analysis).setValue(45d), + newMeasureDto(newViolationMetric, project, analysis).setVariation(1, 46d), + newMeasureDto(nclocMetric, project, analysis).setValue(47d))); + db.commit(); + + String result = ws.newRequest() + .setParam(PARAM_COMPONENT, project.getKey()) + .setParam(PARAM_METRICS, String.join(",", metrics)) + .execute().getInput(); + + assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString()); + } + + private SearchHistoryResponse call() { + SearchHistoryRequest wsRequest = this.wsRequest.build(); + + TestRequest request = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF); + + request.setParam(PARAM_COMPONENT, wsRequest.getComponent()); + request.setParam(PARAM_METRICS, String.join(",", wsRequest.getMetrics())); + setNullable(wsRequest.getFrom(), from -> request.setParam(PARAM_FROM, from)); + setNullable(wsRequest.getTo(), to -> request.setParam(PARAM_TO, to)); + setNullable(wsRequest.getPage(), p -> request.setParam(Param.PAGE, String.valueOf(p))); + setNullable(wsRequest.getPageSize(), ps -> request.setParam(Param.PAGE_SIZE, String.valueOf(ps))); + + try { + return SearchHistoryResponse.parseFrom(request.execute().getInputStream()); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static MetricDto newMetricDtoWithoutOptimization() { + return newMetricDto() + .setWorstValue(null) + .setOptimizedBestValue(false) + .setBestValue(null) + .setUserManaged(false); + } + + private MetricDto insertNclocMetric() { + MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization() + .setKey("ncloc") + .setShortName("Lines of code") + .setDescription("Non Commenting Lines of Code") + .setDomain("Size") + .setValueType("INT") + .setDirection(-1) + .setQualitative(false) + .setHidden(false) + .setUserManaged(false)); + db.commit(); + return metric; + } + + private MetricDto insertComplexityMetric() { + MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization() + .setKey("complexity") + .setShortName("Complexity") + .setDescription("Cyclomatic complexity") + .setDomain("Complexity") + .setValueType("INT") + .setDirection(-1) + .setQualitative(false) + .setHidden(false) + .setUserManaged(false)); + db.commit(); + return metric; + } + + private MetricDto insertNewViolationMetric() { + MetricDto metric = dbClient.metricDao().insert(dbSession, newMetricDtoWithoutOptimization() + .setKey("new_violations") + .setShortName("New issues") + .setDescription("New Issues") + .setDomain("Issues") + .setValueType("INT") + .setDirection(-1) + .setQualitative(true) + .setHidden(false) + .setUserManaged(false)); + db.commit(); + return metric; + } +} diff --git a/sonar-db/src/main/java/org/sonar/db/component/SnapshotDto.java b/sonar-db/src/main/java/org/sonar/db/component/SnapshotDto.java index c84b1176211..635b9118496 100644 --- a/sonar-db/src/main/java/org/sonar/db/component/SnapshotDto.java +++ b/sonar-db/src/main/java/org/sonar/db/component/SnapshotDto.java @@ -265,6 +265,9 @@ public final class SnapshotDto { return this; } + /** + * @return analysis date + */ public Long getCreatedAt() { return createdAt; } diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java index 0daf18d49b1..5539c79a73d 100644 --- a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java +++ b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java @@ -22,7 +22,6 @@ package org.sonar.db.measure; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Optional; import org.apache.ibatis.session.ResultHandler; @@ -30,6 +29,7 @@ import org.sonar.db.Dao; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; +import static java.util.Collections.emptyList; import static org.sonar.db.DatabaseUtils.executeLargeInputs; import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput; @@ -58,7 +58,7 @@ public class MeasureDao implements Dao { */ public List selectByQuery(DbSession dbSession, MeasureQuery query) { if (query.returnsEmpty()) { - return Collections.emptyList(); + return emptyList(); } if (query.isOnComponents()) { return executeLargeInputs( @@ -106,18 +106,28 @@ public class MeasureDao implements Dao { public List selectTreeByQuery(DbSession dbSession, ComponentDto baseComponent, MeasureTreeQuery query) { if (query.returnsEmpty()) { - return Collections.emptyList(); + return emptyList(); } return mapper(dbSession).selectTreeByQuery(query, baseComponent.uuid(), query.getUuidPath(baseComponent)); } - public List selectPastMeasures(DbSession dbSession, - String componentUuid, - String analysisUuid, - Collection metricIds) { + public List selectPastMeasures(DbSession dbSession, String componentUuid, String analysisUuid, Collection metricIds) { + if (metricIds.isEmpty()) { + return emptyList(); + } return executeLargeInputs( metricIds, - ids -> mapper(dbSession).selectPastMeasures(componentUuid, analysisUuid, ids)); + ids -> mapper(dbSession).selectPastMeasuresOnSingleAnalysis(componentUuid, analysisUuid, ids)); + } + + public List selectPastMeasures(DbSession dbSession, String componentUuid, List analysisUuids, List metricIds) { + if (analysisUuids.isEmpty() || metricIds.isEmpty()) { + return emptyList(); + } + + return executeLargeInputs( + analysisUuids, + analyses -> mapper(dbSession).selectPastMeasuresOnSeveralAnalyses(componentUuid, analyses, metricIds)); } /** diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDto.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDto.java index ff4bd806603..9e41defc058 100644 --- a/sonar-db/src/main/java/org/sonar/db/measure/MeasureDto.java +++ b/sonar-db/src/main/java/org/sonar/db/measure/MeasureDto.java @@ -86,6 +86,9 @@ public class MeasureDto { return this; } + /** + * @param index starts at 1 + */ @CheckForNull public Double getVariation(int index) { switch (index) { diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java index 978b8b0c47c..eb282770edc 100644 --- a/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java @@ -40,7 +40,11 @@ public interface MeasureMapper { List selectTreeByQuery(@Param("query") MeasureTreeQuery measureQuery, @Param("baseUuid") String baseUuid, @Param("baseUuidPath") String baseUuidPath); - List selectPastMeasures(@Param("componentUuid") String componentUuid, @Param("analysisUuid") String analysisUuid, @Param("metricIds") List metricIds); + List selectPastMeasuresOnSingleAnalysis(@Param("componentUuid") String componentUuid, @Param("analysisUuid") String analysisUuid, + @Param("metricIds") List metricIds); + + List selectPastMeasuresOnSeveralAnalyses(@Param("componentUuid") String componentUuid, @Param("analysisUuids") Collection analysisUuid, + @Param("metricIds") Collection metricIds); List selectProjectMeasuresOfDeveloper(@Param("developerId") long developerId, @Param("metricIds") Collection metricIds); diff --git a/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java b/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java index f19fed07105..d0a4fbd84d0 100644 --- a/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java +++ b/sonar-db/src/main/java/org/sonar/db/metric/MetricDtoFunctions.java @@ -20,7 +20,6 @@ package org.sonar.db.metric; import java.util.function.Predicate; -import javax.annotation.Nonnull; /** * Common functions on MetricDto @@ -32,15 +31,6 @@ public class MetricDtoFunctions { public static Predicate isOptimizedForBestValue() { - return IsMetricOptimizedForBestValue.INSTANCE; - } - - private enum IsMetricOptimizedForBestValue implements Predicate { - INSTANCE; - - @Override - public boolean test(@Nonnull MetricDto input) { - return input.isOptimizedBestValue() && input.getBestValue() != null; - } + return m -> m != null && m.isOptimizedBestValue() && m.getBestValue() != null; } } diff --git a/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml b/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml index 9b5900bba35..c56d91b0cb5 100644 --- a/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml @@ -128,7 +128,7 @@ - select pm.id as id, pm.metric_id as metricId, pm.person_id as personId, pm.value as value from project_measures pm inner join snapshots analysis on analysis.uuid = pm.analysis_uuid @@ -139,6 +139,17 @@ and pm.person_id is null + +