From a2b6fa49b42c0c0063b88c1c5bed56093c46b070 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Tue, 23 May 2017 13:55:45 +0200 Subject: [PATCH] SONAR-9245 Return leak period in api/components/search_projects --- .../it/projectSearch/SearchProjectsTest.java | 82 +++++++++++-------- .../component/ws/SearchProjectsAction.java | 38 +++++---- .../ws/SearchProjectsActionTest.java | 38 +++++++-- .../src/main/protobuf/ws-components.proto | 1 + 4 files changed, 107 insertions(+), 52 deletions(-) diff --git a/it/it-tests/src/test/java/it/projectSearch/SearchProjectsTest.java b/it/it-tests/src/test/java/it/projectSearch/SearchProjectsTest.java index 4906a0481ce..cf94dbdf026 100644 --- a/it/it-tests/src/test/java/it/projectSearch/SearchProjectsTest.java +++ b/it/it-tests/src/test/java/it/projectSearch/SearchProjectsTest.java @@ -25,7 +25,6 @@ import it.Category6Suite; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; import org.assertj.core.groups.Tuple; import org.junit.After; import org.junit.Before; @@ -40,17 +39,18 @@ import org.sonarqube.ws.client.organization.CreateWsRequest; import org.sonarqube.ws.client.project.CreateRequest; import util.ItUtils; -import static com.sonar.orchestrator.build.SonarScanner.create; import static it.Category6Suite.enableOrganizationsSupport; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; +import static util.ItUtils.concat; import static util.ItUtils.deleteOrganizationsIfExists; import static util.ItUtils.newAdminWsClient; import static util.ItUtils.newProjectKey; import static util.ItUtils.projectDir; import static util.ItUtils.restoreProfile; +import static util.ItUtils.sanitizeTimezones; import static util.ItUtils.setServerProperty; /** @@ -86,7 +86,7 @@ public class SearchProjectsTest { @Test public void filter_projects_by_measure_values() throws Exception { String projectKey = newProjectKey(); - analyzeProject(projectKey,"shared/xoo-sample"); + analyzeProject(projectKey, "shared/xoo-sample"); verifyFilterMatches(projectKey, "ncloc > 1"); verifyFilterMatches(projectKey, "ncloc > 1 and comment_lines < 10000"); @@ -96,18 +96,55 @@ public class SearchProjectsTest { @Test public void find_projects_with_no_data() throws Exception { String projectKey = newProjectKey(); - analyzeProject(projectKey,"shared/xoo-sample"); + analyzeProject(projectKey, "shared/xoo-sample"); verifyFilterMatches(projectKey, "coverage = NO_DATA"); verifyFilterDoesNotMatch("ncloc = NO_DATA"); } + @Test + public void provisioned_projects_should_be_included_to_results() throws Exception { + String projectKey = newProjectKey(); + newAdminWsClient(orchestrator).projects().create(CreateRequest.builder().setKey(projectKey).setName(projectKey).setOrganization(organizationKey).build()); + + SearchProjectsWsResponse response = searchProjects(SearchProjectsRequest.builder().setOrganization(organizationKey).build()); + + assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(projectKey); + } + + @Test + public void return_leak_period_date() throws Exception { + setServerProperty(orchestrator, "sonar.leak.period", "previous_version"); + // This project has a leak period + String projectKey1 = newProjectKey(); + analyzeProject(projectKey1, "shared/xoo-sample", "sonar.projectDate", "2016-12-31"); + analyzeProject(projectKey1, "shared/xoo-sample"); + // This project has only one analysis, so no leak period + String projectKey2 = newProjectKey(); + analyzeProject(projectKey2, "shared/xoo-sample"); + // This project is provisioned, so has no leak period + String projectKey3 = newProjectKey(); + newAdminWsClient(orchestrator).projects().create(CreateRequest.builder().setKey(projectKey3).setName(projectKey3).setOrganization(organizationKey).build()); + + SearchProjectsWsResponse response = searchProjects( + SearchProjectsRequest.builder().setAdditionalFields(singletonList("leakPeriodDate")).setOrganization(organizationKey).build()); + + assertThat(response.getComponentsList()).extracting(Component::getKey, Component::hasLeakPeriodDate) + .containsOnly( + tuple(projectKey1, true), + tuple(projectKey2, false), + tuple(projectKey3, false)); + Component project1 = response.getComponentsList().stream().filter(component -> component.getKey().equals(projectKey1)).findFirst() + .orElseThrow(() -> new IllegalStateException("Project1 is not found")); + assertThat(sanitizeTimezones(project1.getLeakPeriodDate())).isEqualTo("2016-12-31T00:00:00+0000"); + } + @Test public void filter_by_text_query() throws IOException { - orchestrator.executeBuild(create(projectDir("shared/xoo-sample"), "sonar.projectKey", "project1", "sonar.projectName", "apachee")); - orchestrator.executeBuild(create(projectDir("shared/xoo-sample"), "sonar.projectKey", "project2", "sonar.projectName", "Apache")); - orchestrator.executeBuild(create(projectDir("shared/xoo-multi-modules-sample"), "sonar.projectKey", "project3", "sonar.projectName", "Apache Foundation")); - orchestrator.executeBuild(create(projectDir("shared/xoo-multi-modules-sample"), "sonar.projectKey", "project4", "sonar.projectName", "Windows")); + analyzeProject("project1", "shared/xoo-sample", "sonar.projectName", "apachee"); + analyzeProject("project2", "shared/xoo-sample", "sonar.projectName", "Apache"); + analyzeProject("project3", "shared/xoo-multi-modules-sample", "sonar.projectName", "Apache Foundation"); + analyzeProject("project4", "shared/xoo-multi-modules-sample", "sonar.projectName", "Windows"); // Search only by text query assertThat(searchProjects("query = \"apache\"").getComponentsList()).extracting(Component::getKey).containsExactly("project2", "project3", "project1"); @@ -133,16 +170,6 @@ public class SearchProjectsTest { .containsOnly(tuple("*-1000.0", 0L), tuple("1000.0-10000.0", 0L), tuple("10000.0-100000.0", 0L), tuple("100000.0-500000.0", 0L), tuple("500000.0-*", 0L)); } - @Test - public void provisioned_projects_should_be_included_to_results() throws Exception { - String projectKey = newProjectKey(); - newAdminWsClient(orchestrator).projects().create(CreateRequest.builder().setKey(projectKey).setName(projectKey).setOrganization(organizationKey).build()); - - SearchProjectsWsResponse response = searchProjects(SearchProjectsRequest.builder().setOrganization(organizationKey).build()); - - assertThat(response.getComponentsList()).extracting(Component::getKey).contains(projectKey); - } - @Test public void should_return_facets() throws Exception { analyzeProject(newProjectKey(), "shared/xoo-sample"); @@ -211,11 +238,11 @@ public class SearchProjectsTest { setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis"); // This project has no duplication on new code String projectKey1 = newProjectKey(); - analyzeProject(projectKey1, "shared/xoo-sample", "2016-12-31"); + analyzeProject(projectKey1, "shared/xoo-sample", "sonar.projectDate", "2016-12-31"); analyzeProject(projectKey1, "shared/xoo-sample"); // This project has 0% duplication on new code String projectKey2 = newProjectKey(); - analyzeProject(projectKey2, "projectSearch/xoo-history-v1", "2016-12-31"); + analyzeProject(projectKey2, "projectSearch/xoo-history-v1", "sonar.projectDate", "2016-12-31"); analyzeProject(projectKey2, "projectSearch/xoo-history-v2"); SearchProjectsWsResponse response = searchProjects(SearchProjectsRequest.builder().setOrganization(organizationKey).setFacets(asList( @@ -266,23 +293,14 @@ public class SearchProjectsTest { assertThat(facet.getValuesList()).extracting(Common.FacetValue::getVal, Common.FacetValue::getCount).containsExactlyInAnyOrder(values); } - private void analyzeProject(String projectKey, String relativePath) { - analyzeProject(projectKey, relativePath, null); - } - - private void analyzeProject(String projectKey, String relativePath, @Nullable String analysisDate) { + private void analyzeProject(String projectKey, String relativePath, String... properties) { List keyValueProperties = new ArrayList<>(asList( "sonar.projectKey", projectKey, "sonar.organization", organizationKey, "sonar.profile", "with-many-rules", "sonar.login", "admin", "sonar.password", "admin", - "sonar.scm.disabled", "false" - )); - if (analysisDate != null) { - keyValueProperties.add("sonar.projectDate"); - keyValueProperties.add(analysisDate); - } - orchestrator.executeBuild(SonarScanner.create(projectDir(relativePath), keyValueProperties.toArray(new String[0]))); + "sonar.scm.disabled", "false")); + orchestrator.executeBuild(SonarScanner.create(projectDir(relativePath), concat(keyValueProperties.toArray(new String[0]), properties))); } private SearchProjectsWsResponse searchProjects(String filter) throws IOException { diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java index 76827619b7c..6d515dc7074 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java @@ -38,7 +38,6 @@ 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.utils.DateUtils; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -51,7 +50,6 @@ import org.sonar.server.component.ws.FilterParser.Criterion; import org.sonar.server.es.Facets; import org.sonar.server.es.SearchIdResult; import org.sonar.server.es.SearchOptions; -import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.measure.index.ProjectMeasuresIndex; import org.sonar.server.measure.index.ProjectMeasuresQuery; import org.sonar.server.project.Visibility; @@ -68,12 +66,15 @@ import static java.util.Collections.emptyMap; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; import static org.sonar.api.server.ws.WebService.Param.FIELDS; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.core.util.Protobuf.setNullable; import static org.sonar.core.util.stream.MoreCollectors.toSet; import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.IS_FAVORITE_CRITERION; import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; import static org.sonar.server.measure.index.ProjectMeasuresIndex.SUPPORTED_FACETS; import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_LAST_ANALYSIS_DATE; import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_NAME; +import static org.sonar.server.ws.WsUtils.checkFound; import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FILTER; @@ -86,7 +87,8 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_TAGS; public class SearchProjectsAction implements ComponentsWsAction { private static final String ANALYSIS_DATE = "analysisDate"; - private static final Set POSSIBLE_FIELDS = newHashSet(ANALYSIS_DATE); + private static final String LEAK_PERIOD_DATE = "leakPeriodDate"; + private static final Set POSSIBLE_FIELDS = newHashSet(ANALYSIS_DATE, LEAK_PERIOD_DATE); private final DbClient dbClient; private final ProjectMeasuresIndex index; @@ -112,8 +114,8 @@ public class SearchProjectsAction implements ComponentsWsAction { new Change("6.4", format("The '%s' parameter accepts '%s' to filter by language", FILTER_LANGUAGES, PARAM_FILTER)), new Change("6.4", "The 'visibility' field is added"), new Change("6.5", "The 'filter' parameter now allows 'NO_DATA' as value for numeric metrics"), - new Change("6.5", "Added the option 'analysisDate' for the 'sort' parameter") - ) + new Change("6.5", "Added the option 'analysisDate' for the 'sort' parameter"), + new Change("6.5", format("Value '%s' is added to parameter '%s'", LEAK_PERIOD_DATE, FIELDS))) .setHandler(this); action.createFieldsParam(POSSIBLE_FIELDS) @@ -272,7 +274,7 @@ public class SearchProjectsAction implements ComponentsWsAction { } private Map getSnapshots(DbSession dbSession, SearchProjectsRequest request, List projectUuids) { - if (request.getAdditionalFields().contains(ANALYSIS_DATE)) { + if (request.getAdditionalFields().contains(ANALYSIS_DATE) || request.getAdditionalFields().contains(LEAK_PERIOD_DATE)) { return dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids) .stream() .collect(MoreCollectors.uniqueIndex(SnapshotDto::getComponentUuid)); @@ -291,14 +293,14 @@ public class SearchProjectsAction implements ComponentsWsAction { if (httpRequest.hasParam(Param.FACETS)) { request.setFacets(httpRequest.paramAsStrings(Param.FACETS)); } - if (httpRequest.hasParam(Param.FIELDS)) { + if (httpRequest.hasParam(FIELDS)) { request.setAdditionalFields(httpRequest.paramAsStrings(FIELDS)); } return request.build(); } private SearchProjectsWsResponse buildResponse(SearchProjectsRequest request, SearchResults searchResults, Map organizationsByUuid) { - Function dbToWsComponent = new DbToWsComponent(organizationsByUuid, searchResults.favoriteProjectUuids, searchResults.analysisByProjectUuid, + Function dbToWsComponent = new DbToWsComponent(request, organizationsByUuid, searchResults.favoriteProjectUuids, searchResults.analysisByProjectUuid, userSession.isLoggedIn()); return Stream.of(SearchProjectsWsResponse.newBuilder()) @@ -392,14 +394,16 @@ public class SearchProjectsAction implements ComponentsWsAction { } private static class DbToWsComponent implements Function { + private final SearchProjectsRequest request; private final Component.Builder wsComponent; private final Map organizationsByUuid; private final Set favoriteProjectUuids; private final boolean isUserLoggedIn; private final Map analysisByProjectUuid; - private DbToWsComponent(Map organizationsByUuid, Set favoriteProjectUuids, Map analysisByProjectUuid, - boolean isUserLoggedIn) { + private DbToWsComponent(SearchProjectsRequest request, Map organizationsByUuid, Set favoriteProjectUuids, + Map analysisByProjectUuid, boolean isUserLoggedIn) { + this.request = request; this.analysisByProjectUuid = analysisByProjectUuid; this.wsComponent = Component.newBuilder(); this.organizationsByUuid = organizationsByUuid; @@ -409,10 +413,9 @@ public class SearchProjectsAction implements ComponentsWsAction { @Override public Component apply(ComponentDto dbComponent) { - OrganizationDto organizationDto = organizationsByUuid.get(dbComponent.getOrganizationUuid()); - if (organizationDto == null) { - throw new NotFoundException(format("Organization with uuid '%s' not found", dbComponent.getOrganizationUuid())); - } + String organizationUuid = dbComponent.getOrganizationUuid(); + OrganizationDto organizationDto = organizationsByUuid.get(organizationUuid); + checkFound(organizationDto, "Organization with uuid '%s' not found", organizationUuid); wsComponent .clear() .setOrganization(organizationDto.getKey()) @@ -424,7 +427,12 @@ public class SearchProjectsAction implements ComponentsWsAction { SnapshotDto snapshotDto = analysisByProjectUuid.get(dbComponent.uuid()); if (snapshotDto != null) { - wsComponent.setAnalysisDate(DateUtils.formatDateTime(snapshotDto.getCreatedAt())); + if (request.getAdditionalFields().contains(ANALYSIS_DATE)) { + wsComponent.setAnalysisDate(formatDateTime(snapshotDto.getCreatedAt())); + } + if (request.getAdditionalFields().contains(LEAK_PERIOD_DATE)) { + setNullable(snapshotDto.getPeriodDate(), leakPeriodDate -> wsComponent.setLeakPeriodDate(formatDateTime(leakPeriodDate))); + } } if (isUserLoggedIn) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index f39e7092cca..59bb204ac1c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -152,8 +152,8 @@ public class SearchProjectsActionTest { tuple("6.4", "The 'languages' parameter accepts 'filter' to filter by language"), tuple("6.4", "The 'visibility' field is added"), tuple("6.5", "The 'filter' parameter now allows 'NO_DATA' as value for numeric metrics"), - tuple("6.5", "Added the option 'analysisDate' for the 'sort' parameter") - ); + tuple("6.5", "Added the option 'analysisDate' for the 'sort' parameter"), + tuple("6.5", "Value 'leakPeriodDate' is added to parameter 'f'")); Param organization = def.param("organization"); assertThat(organization.isRequired()).isFalse(); @@ -171,7 +171,7 @@ public class SearchProjectsActionTest { Param additionalFields = def.param("f"); assertThat(additionalFields.defaultValue()).isNull(); - assertThat(additionalFields.possibleValues()).containsOnly("analysisDate"); + assertThat(additionalFields.possibleValues()).containsOnly("analysisDate", "leakPeriodDate"); Param facets = def.param("facets"); assertThat(facets.defaultValue()).isNull(); @@ -996,8 +996,36 @@ public class SearchProjectsActionTest { SearchProjectsWsResponse result = call(request.setAdditionalFields(singletonList("analysisDate"))); - assertThat(result.getComponentsList()).extracting(Component::getAnalysisDate) - .containsOnly(formatDateTime(new Date(20_000_000_000L)), formatDateTime(new Date(30_000_000_000L)), ""); + assertThat(result.getComponentsList()).extracting(Component::getKey, Component::hasAnalysisDate, Component::getAnalysisDate) + .containsOnly( + tuple(project1.getKey(), true, formatDateTime(new Date(20_000_000_000L))), + tuple(project2.getKey(), true, formatDateTime(new Date(30_000_000_000L))), + tuple(project3.getKey(), false, "")); + } + + @Test + public void return_leak_period_date() { + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + ComponentDto project1 = db.components().insertPublicProject(organization); + db.components().insertSnapshot(project1, snapshot -> snapshot.setPeriodDate(10_000_000_000L)); + authorizationIndexerTester.allowOnlyAnyone(project1); + // No leak period + ComponentDto project2 = db.components().insertPublicProject(organization); + db.components().insertSnapshot(project2, snapshot -> snapshot.setPeriodDate(null)); + authorizationIndexerTester.allowOnlyAnyone(project2); + // No snapshot on project 3 + ComponentDto project3 = db.components().insertPublicProject(organization); + authorizationIndexerTester.allowOnlyAnyone(project3); + projectMeasuresIndexer.indexOnStartup(null); + + SearchProjectsWsResponse result = call(request.setAdditionalFields(singletonList("leakPeriodDate"))); + + assertThat(result.getComponentsList()).extracting(Component::getKey, Component::hasLeakPeriodDate, Component::getLeakPeriodDate) + .containsOnly( + tuple(project1.getKey(), true, formatDateTime(new Date(10_000_000_000L))), + tuple(project2.getKey(), false, ""), + tuple(project3.getKey(), false, "")); } @Test diff --git a/sonar-ws/src/main/protobuf/ws-components.proto b/sonar-ws/src/main/protobuf/ws-components.proto index 3159ed3ba95..58b02d9687c 100644 --- a/sonar-ws/src/main/protobuf/ws-components.proto +++ b/sonar-ws/src/main/protobuf/ws-components.proto @@ -117,6 +117,7 @@ message Component { optional string analysisDate = 13; optional Tags tags = 14; optional string visibility = 15; + optional string leakPeriodDate = 16; message Tags { repeated string tags = 1; -- 2.39.5