From 629f228080b114ea57d311e41d3b4c76cb493195 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Tue, 6 Jun 2017 16:06:35 +0200 Subject: [PATCH] SONAR-9377 Fix facet values of projects page when using text query --- .../it/projectSearch/SearchProjectsTest.java | 36 +- .../measure/index/ProjectMeasuresIndex.java | 9 +- .../index/ProjectsTextSearchQueryFactory.java | 129 +++++++ .../ProjectMeasuresIndexTextSearchTest.java | 338 ++++++++++++++++++ 4 files changed, 502 insertions(+), 10 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java 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 16fd31cdb87..619c6b5dc24 100644 --- a/it/it-tests/src/test/java/it/projectSearch/SearchProjectsTest.java +++ b/it/it-tests/src/test/java/it/projectSearch/SearchProjectsTest.java @@ -20,17 +20,20 @@ package it.projectSearch; import com.sonar.orchestrator.Orchestrator; -import com.sonar.orchestrator.build.SonarScanner; import it.Category4Suite; import java.io.IOException; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; +import org.sonarqube.ws.Common.FacetValue; import org.sonarqube.ws.WsComponents.Component; import org.sonarqube.ws.WsComponents.SearchProjectsWsResponse; import org.sonarqube.ws.client.component.SearchProjectsRequest; +import static com.sonar.orchestrator.build.SonarScanner.create; +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.newAdminWsClient; import static util.ItUtils.projectDir; @@ -52,7 +55,7 @@ public class SearchProjectsTest { @Test public void filter_projects_by_measure_values() throws Exception { - orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"))); + orchestrator.executeBuild(create(projectDir("shared/xoo-sample"))); verifyFilterMatches("ncloc > 1"); verifyFilterMatches("ncloc > 1 and comment_lines < 10000"); @@ -68,6 +71,35 @@ public class SearchProjectsTest { assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY); } + @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")); + + // Search only by text query + assertThat(searchProjects("query = \"apache\"").getComponentsList()).extracting(Component::getKey).containsExactly("project2", "project3", "project1"); + assertThat(searchProjects("query = \"pAch\"").getComponentsList()).extracting(Component::getKey).containsExactly("project2", "project3", "project1"); + assertThat(searchProjects("query = \"hee\"").getComponentsList()).extracting(Component::getKey).containsExactly("project1"); + assertThat(searchProjects("query = \"project1\"").getComponentsList()).extracting(Component::getKey).containsExactly("project1"); + assertThat(searchProjects("query = \"unknown\"").getComponentsList()).isEmpty(); + + // Search by metric criteria and text query + assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"pAch\" AND ncloc > 50").build()).getComponentsList()) + .extracting(Component::getKey).containsExactly("project3"); + assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"nd\" AND ncloc > 50").build()).getComponentsList()) + .extracting(Component::getKey).containsExactly("project3", "project4"); + assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"unknown\" AND ncloc > 50").build()).getComponentsList()).isEmpty();; + + // Check facets + assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"apache\"").setFacets(singletonList("ncloc")).build()).getFacets().getFacets(0).getValuesList()) + .extracting(FacetValue::getVal, FacetValue::getCount) + .containsOnly(tuple("*-1000.0", 3L), tuple("1000.0-10000.0", 0L), tuple("10000.0-100000.0", 0L), tuple("100000.0-500000.0", 0L), tuple("500000.0-*", 0L)); + assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"unknown\"").setFacets(singletonList("ncloc")).build()).getFacets().getFacets(0) + .getValuesList()).extracting(FacetValue::getVal, FacetValue::getCount) + .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)); + } private SearchProjectsWsResponse searchProjects(String filter) throws IOException { return searchProjects(SearchProjectsRequest.builder().setFilter(filter).build()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java index 2f42cfeb00c..9fa14bd5785 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java @@ -47,8 +47,6 @@ import org.sonar.server.es.EsClient; import org.sonar.server.es.SearchIdResult; import org.sonar.server.es.SearchOptions; import org.sonar.server.es.StickyFacetBuilder; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; -import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory; import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; import org.sonar.server.permission.index.AuthorizationTypeSupport; @@ -279,12 +277,7 @@ public class ProjectMeasuresIndex { if (!queryText.isPresent()) { return Optional.empty(); } - ComponentTextSearchQueryFactory.ComponentTextSearchQuery componentTextSearchQuery = ComponentTextSearchQueryFactory.ComponentTextSearchQuery.builder() - .setQueryText(queryText.get()) - .setFieldKey(FIELD_KEY) - .setFieldName(FIELD_NAME) - .build(); - return Optional.of(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, ComponentTextSearchFeatureRepertoire.values())); + return Optional.of(ProjectsTextSearchQueryFactory.createQuery(queryText.get())); } private static QueryBuilder toValueQuery(MetricCriterion criterion) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java new file mode 100644 index 00000000000..3b849d0bb53 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java @@ -0,0 +1,129 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.measure.index; + +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; +import org.apache.commons.lang.StringUtils; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.MatchQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.sonar.server.es.DefaultIndexSettings; + +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchQuery; +import static org.elasticsearch.index.query.QueryBuilders.prefixQuery; +import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_GRAMS_ANALYZER; +import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; + +/** + * This class is used in order to do some advanced full text search on projects key and name + */ +class ProjectsTextSearchQueryFactory { + + private ProjectsTextSearchQueryFactory() { + // Only static methods + } + + static QueryBuilder createQuery(String queryText) { + BoolQueryBuilder featureQuery = boolQuery(); + Arrays.stream(ComponentTextSearchFeature.values()) + .map(f -> f.getQuery(queryText)) + .forEach(featureQuery::should); + return featureQuery; + } + + private enum ComponentTextSearchFeature { + + EXACT_IGNORE_CASE { + @Override + QueryBuilder getQuery(String queryText) { + return matchQuery(SORTABLE_ANALYZER.subField(FIELD_NAME), queryText) + .boost(2.5f); + } + }, + PREFIX { + @Override + QueryBuilder getQuery(String queryText) { + return prefixAndPartialQuery(queryText, FIELD_NAME, FIELD_NAME) + .boost(2f); + } + }, + PREFIX_IGNORE_CASE { + @Override + QueryBuilder getQuery(String queryText) { + String lowerCaseQueryText = queryText.toLowerCase(Locale.getDefault()); + return prefixAndPartialQuery(lowerCaseQueryText, SORTABLE_ANALYZER.subField(FIELD_NAME), FIELD_NAME) + .boost(3f); + } + }, + PARTIAL { + @Override + QueryBuilder getQuery(String queryText) { + BoolQueryBuilder queryBuilder = boolQuery(); + split(queryText) + .map(text -> partialTermQuery(text, FIELD_NAME)) + .forEach(queryBuilder::must); + return queryBuilder + .boost(0.5f); + } + }, + KEY { + @Override + QueryBuilder getQuery(String queryText) { + return matchQuery(SORTABLE_ANALYZER.subField(FIELD_KEY), queryText) + .boost(50f); + } + }; + + abstract QueryBuilder getQuery(String queryText); + + protected Stream split(String queryText) { + return Arrays.stream( + queryText.split(DefaultIndexSettings.SEARCH_TERM_TOKENIZER_PATTERN)) + .filter(StringUtils::isNotEmpty); + } + + protected BoolQueryBuilder prefixAndPartialQuery(String queryText, String fieldName, String originalFieldName) { + BoolQueryBuilder queryBuilder = boolQuery(); + AtomicBoolean first = new AtomicBoolean(true); + split(queryText) + .map(queryTerm -> { + if (first.getAndSet(false)) { + return prefixQuery(fieldName, queryTerm); + } + return partialTermQuery(queryTerm, originalFieldName); + }) + .forEach(queryBuilder::must); + return queryBuilder; + } + + protected MatchQueryBuilder partialTermQuery(String queryTerm, String fieldName) { + // We will truncate the search to the maximum length of nGrams in the index. + // Otherwise the search would for sure not find any results. + String truncatedQuery = StringUtils.left(queryTerm, DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH); + return matchQuery(SEARCH_GRAMS_ANALYZER.subField(fieldName), truncatedQuery); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java new file mode 100644 index 00000000000..b34c381afed --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java @@ -0,0 +1,338 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.measure.index; + +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.MapSettings; +import org.sonar.api.resources.Qualifiers; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.permission.index.AuthorizationTypeSupport; +import org.sonar.server.permission.index.PermissionIndexerDao; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.tester.UserSessionRule; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.server.component.ws.FilterParser.Operator.GT; +import static org.sonar.server.component.ws.FilterParser.Operator.LT; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; + +public class ProjectMeasuresIndexTextSearchTest { + + private static final String NCLOC = "ncloc"; + + private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); + + @Rule + public EsTester es = new EsTester(new ProjectMeasuresIndexDefinition(new MapSettings())); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer); + private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new AuthorizationTypeSupport(userSession)); + + @Test + public void match_exact_case_insensitive_name() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")), + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube"))); + + assertTextQueryResults("Apache Struts", "struts"); + assertTextQueryResults("APACHE STRUTS", "struts"); + assertTextQueryResults("APACHE struTS", "struts"); + } + + @Test + public void match_from_sub_name() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("truts", "struts"); + assertTextQueryResults("pache", "struts"); + assertTextQueryResults("apach", "struts"); + assertTextQueryResults("che stru", "struts"); + } + + @Test + public void match_name_with_dot() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache.Struts"))); + + assertTextQueryResults("apache struts", "struts"); + } + + @Test + public void match_partial_name() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("XstrutsxXjavax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_prefix_word1() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.java"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_suffix_word1() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("StrutsObject.java"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_prefix_word2() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.xjava"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_suffix_word2() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStrutsObject.xjavax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_subset_of_document_terms() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Some.Struts.Project.java.old"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_match_prefix_and_suffix_everywhere() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.javax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void ignore_empty_words() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Struts"))); + + assertTextQueryResults(" struts \n \n\n", "struts"); + } + + @Test + public void match_name_from_prefix() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("apach", "struts"); + assertTextQueryResults("ApA", "struts"); + assertTextQueryResults("AP", "struts"); + } + + @Test + public void match_name_from_two_words() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("ApacheStrutsFoundation"))); + + assertTextQueryResults("apache struts", "project"); + assertTextQueryResults("struts apache", "project"); + // Only one word is matching + assertNoResults("apache plugin"); + assertNoResults("project struts"); + } + + @Test + public void match_long_name() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("LongNameLongNameLongNameLongNameSonarQube")), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("LongNameLongNameLongNameLongNameSonarQubeX"))); + + assertTextQueryResults("LongNameLongNameLongNameLongNameSonarQube", "project1", "project2"); + } + + @Test + public void match_name_with_two_characters() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("st", "struts"); + assertTextQueryResults("tr", "struts"); + } + + @Test + public void match_exact_case_insensitive_key() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setKey("project1")), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setKey("project2"))); + + assertTextQueryResults("project1", "project1"); + assertTextQueryResults("PROJECT1", "project1"); + assertTextQueryResults("pRoJecT1", "project1"); + } + + @Test + public void match_key_with_dot() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setKey("org.sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setKey("sonarqube"))); + + assertTextQueryResults("org.sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org-sonarqube"); + assertNoResults("org:sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_with_dash() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setKey("org-sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setKey("sonarqube"))); + + assertTextQueryResults("org-sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org.sonarqube"); + assertNoResults("org:sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_with_colon() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setKey("org:sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setKey("sonarqube"))); + + assertTextQueryResults("org:sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org-sonarqube"); + assertNoResults("org_sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_having_all_special_characters() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setKey("org.sonarqube:sonar-sérvèr_ç"))); + + assertTextQueryResults("org.sonarqube:sonar-sérvèr_ç", "sonarqube"); + } + + @Test + public void does_not_match_partial_key() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("some name").setKey("theKey"))); + + assertNoResults("theke"); + assertNoResults("hekey"); + } + + @Test + public void facets_take_into_account_text_search() { + index( + // docs with ncloc<1K + newDoc(newPrivateProjectDto(ORG).setName("Windows").setKey("project1"), NCLOC, 0d), + newDoc(newPrivateProjectDto(ORG).setName("apachee").setKey("project2"), NCLOC, 999d), + // docs with ncloc>=1K and ncloc<10K + newDoc(newPrivateProjectDto(ORG).setName("Apache").setKey("project3"), NCLOC, 1_000d), + // docs with ncloc>=100K and ncloc<500K + newDoc(newPrivateProjectDto(ORG).setName("Apache Foundation").setKey("project4"), NCLOC, 100_000d)); + + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache"), 1L, 1L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("PAch"), 1L, 1L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache foundation"), 0L, 0L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project3"), 0L, 1L, 0L, 0L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project"), 0L, 0L, 0L, 0L, 0L); + } + + @Test + public void filter_by_metric_take_into_account_text_search() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setKey("project1"), NCLOC, 30_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setKey("project2"), NCLOC, 40_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project3").setName("Apache").setKey("project3"), NCLOC, 50_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project4").setName("Apache").setKey("project4"), NCLOC, 60_000d)); + + assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(new MetricCriterion(NCLOC, GT, 20_000d)), "project3", "project4", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(new MetricCriterion(NCLOC, LT, 55_000d)), "project3", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("PAC").addMetricCriterion(new MetricCriterion(NCLOC, LT, 55_000d)), "project3", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("apachee").addMetricCriterion(new MetricCriterion(NCLOC, GT, 30_000d)), "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("unknown").addMetricCriterion(new MetricCriterion(NCLOC, GT, 20_000d))); + } + + private void index(ProjectMeasuresDoc... docs) { + es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); + stream(docs).forEach(doc -> { + PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), System.currentTimeMillis(), Qualifiers.PROJECT); + access.allowAnyone(); + authorizationIndexerTester.allow(access); + }); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project) { + return new ProjectMeasuresDoc() + .setOrganizationUuid(project.getOrganizationUuid()) + .setId(project.uuid()) + .setKey(project.key()) + .setName(project.name()); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); + } + + private static Map newMeasure(String key, Object value) { + return ImmutableMap.of("key", key, "value", value); + } + + private void assertResults(ProjectMeasuresQuery query, String... expectedProjectUuids) { + List result = underTest.search(query, new SearchOptions()).getIds(); + assertThat(result).containsExactly(expectedProjectUuids); + } + + private void assertTextQueryResults(String queryText, String... expectedProjectUuids) { + assertResults(new ProjectMeasuresQuery().setQueryText(queryText), expectedProjectUuids); + } + + private void assertNoResults(String queryText) { + assertTextQueryResults(queryText); + } + + private void assertNclocFacet(ProjectMeasuresQuery query, Long... facetExpectedValues) { + checkArgument(facetExpectedValues.length == 5, "5 facet values is required"); + Facets facets = underTest.search(query, new SearchOptions().addFacets(NCLOC)).getFacets(); + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", facetExpectedValues[0]), + entry("1000.0-10000.0", facetExpectedValues[1]), + entry("10000.0-100000.0", facetExpectedValues[2]), + entry("100000.0-500000.0", facetExpectedValues[3]), + entry("500000.0-*", facetExpectedValues[4])); + } +} -- 2.39.5