| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.solr.cloud; |
| |
| import java.io.IOException; |
| import java.lang.invoke.MethodHandles; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| import java.util.Set; |
| import java.util.TreeSet; |
| |
| import org.apache.commons.io.FilenameUtils; |
| import org.apache.lucene.util.TestUtil; |
| import org.apache.solr.client.solrj.SolrClient; |
| import org.apache.solr.client.solrj.SolrServerException; |
| import org.apache.solr.client.solrj.embedded.JettySolrRunner; |
| import org.apache.solr.client.solrj.impl.CloudSolrClient; |
| import org.apache.solr.client.solrj.impl.HttpSolrClient; |
| import org.apache.solr.client.solrj.request.CollectionAdminRequest; |
| import org.apache.solr.client.solrj.response.QueryResponse; |
| import org.apache.solr.common.SolrDocument; |
| import org.apache.solr.common.SolrDocumentList; |
| import org.apache.solr.common.SolrInputDocument; |
| import org.apache.solr.common.params.ModifiableSolrParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.response.transform.DocTransformer; |
| import org.apache.solr.response.transform.RawValueTransformerFactory; |
| import org.apache.solr.response.transform.TransformerFactory; |
| import org.apache.solr.util.RandomizeSSL; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** @see TestCloudPseudoReturnFields */ |
| @RandomizeSSL(clientAuth=0.0,reason="client auth uses too much RAM") |
| public class TestRandomFlRTGCloud extends SolrCloudTestCase { |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| private static final String DEBUG_LABEL = MethodHandles.lookup().lookupClass().getName(); |
| private static final String COLLECTION_NAME = DEBUG_LABEL + "_collection"; |
| |
| /** A basic client for operations at the cloud level, default collection will be set */ |
| private static CloudSolrClient CLOUD_CLIENT; |
| /** One client per node */ |
| private static final List<HttpSolrClient> CLIENTS = Collections.synchronizedList(new ArrayList<>(5)); |
| |
| /** Always included in fl so we can vet what doc we're looking at */ |
| private static final FlValidator ID_VALIDATOR = new SimpleFieldValueValidator("id"); |
| |
| /** Since nested documents are not tested, when _root_ is declared in schema, it is always the same as id */ |
| private static final FlValidator ROOT_VALIDATOR = new RenameFieldValueValidator("id" , "_root_"); |
| |
| /** |
| * Types of things we will randomly ask for in fl param, and validate in response docs. |
| * |
| * @see #addRandomFlValidators |
| */ |
| private static final List<FlValidator> FL_VALIDATORS = Collections.unmodifiableList |
| (Arrays.<FlValidator>asList( |
| new GlobValidator("*"), |
| new GlobValidator("*_i"), |
| new GlobValidator("*_s"), |
| new GlobValidator("a*"), |
| new DocIdValidator(), |
| new DocIdValidator("my_docid_alias"), |
| new ShardValidator(), |
| new ShardValidator("my_shard_alias"), |
| new ValueAugmenterValidator(42), |
| new ValueAugmenterValidator(1976, "val_alias"), |
| // |
| new RenameFieldValueValidator("id", "my_id_alias"), |
| new SimpleFieldValueValidator("aaa_i"), |
| new RenameFieldValueValidator("bbb_i", "my_int_field_alias"), |
| new SimpleFieldValueValidator("ccc_s"), |
| new RenameFieldValueValidator("ddd_s", "my_str_field_alias"), |
| // |
| // SOLR-9376: RawValueTransformerFactory doesn't work in cloud mode |
| // |
| // new RawFieldValueValidator("json", "eee_s", "my_json_field_alias"), |
| // new RawFieldValueValidator("json", "fff_s"), |
| // new RawFieldValueValidator("xml", "ggg_s", "my_xml_field_alias"), |
| // new RawFieldValueValidator("xml", "hhh_s"), |
| // |
| new NotIncludedValidator("bogus_unused_field_ss"), |
| new NotIncludedValidator("bogus_alias","bogus_alias:other_bogus_field_i"), |
| new NotIncludedValidator("bogus_raw_alias","bogus_raw_alias:[xml f=bogus_raw_field_ss]"), |
| // |
| new FunctionValidator("aaa_i"), // fq field |
| new FunctionValidator("aaa_i", "func_aaa_alias"), |
| new GeoTransformerValidator("geo_1_srpt"), |
| new GeoTransformerValidator("geo_2_srpt","my_geo_alias"), |
| new ExplainValidator(), |
| new ExplainValidator("explain_alias"), |
| new SubQueryValidator(), |
| new NotIncludedValidator("score"), |
| new NotIncludedValidator("score","score_alias:score"))); |
| |
| @BeforeClass |
| public static void createMiniSolrCloudCluster() throws Exception { |
| |
| // 50% runs use single node/shard a FL_VALIDATORS with all validators known to work on single node |
| // 50% runs use multi node/shard with FL_VALIDATORS only containing stuff that works in cloud |
| final boolean singleCoreMode = random().nextBoolean(); |
| |
| // (asuming multi core multi replicas shouldn't matter (assuming multi node) ... |
| final int repFactor = singleCoreMode ? 1 : (usually() ? 1 : 2); |
| // ... but we definitely want to ensure forwarded requests to other shards work ... |
| final int numShards = singleCoreMode ? 1 : 2; |
| // ... including some forwarded requests from nodes not hosting a shard |
| final int numNodes = 1 + (singleCoreMode ? 0 : (numShards * repFactor)); |
| |
| final String configName = DEBUG_LABEL + "_config-set"; |
| final Path configDir = Paths.get(TEST_HOME(), "collection1", "conf"); |
| |
| configureCluster(numNodes).addConfig(configName, configDir).configure(); |
| |
| CLOUD_CLIENT = cluster.getSolrClient(); |
| CLOUD_CLIENT.setDefaultCollection(COLLECTION_NAME); |
| |
| CollectionAdminRequest.createCollection(COLLECTION_NAME, configName, numShards, repFactor) |
| .withProperty("config", "solrconfig-tlog.xml") |
| .withProperty("schema", "schema-psuedo-fields.xml") |
| .process(CLOUD_CLIENT); |
| |
| cluster.waitForActiveCollection(COLLECTION_NAME, numShards, repFactor * numShards); |
| |
| for (JettySolrRunner jetty : cluster.getJettySolrRunners()) { |
| CLIENTS.add(getHttpSolrClient(jetty.getBaseUrl() + "/" + COLLECTION_NAME + "/")); |
| } |
| } |
| |
| @AfterClass |
| private static void afterClass() throws Exception { |
| if (null != CLOUD_CLIENT) { |
| CLOUD_CLIENT.close(); |
| CLOUD_CLIENT = null; |
| } |
| for (HttpSolrClient client : CLIENTS) { |
| client.close(); |
| } |
| CLIENTS.clear(); |
| } |
| |
| /** |
| * Tests that all TransformerFactories that are implicitly provided by Solr are tested in this class |
| * |
| * @see FlValidator#getDefaultTransformerFactoryName |
| * @see #FL_VALIDATORS |
| * @see TransformerFactory#defaultFactories |
| */ |
| public void testCoverage() throws Exception { |
| final Set<String> implicit = new LinkedHashSet<>(); |
| for (String t : TransformerFactory.defaultFactories.keySet()) { |
| implicit.add(t); |
| } |
| |
| final Set<String> covered = new LinkedHashSet<>(); |
| for (FlValidator v : FL_VALIDATORS) { |
| String t = v.getDefaultTransformerFactoryName(); |
| if (null != t) { |
| covered.add(t); |
| } |
| } |
| |
| // items should only be added to this list if it's known that they do not work with RTG |
| // and a specific Jira for fixing this is listed as a comment |
| final List<String> knownBugs = Arrays.asList |
| ( "xml","json", // SOLR-9376 |
| "child" // way to complicatd to vet with this test, see SOLR-9379 instead |
| ); |
| |
| for (String buggy : knownBugs) { |
| assertFalse(buggy + " is listed as a being a known bug, " + |
| "but it exists in the set of 'covered' TransformerFactories", |
| covered.contains(buggy)); |
| assertTrue(buggy + " is listed as a known bug, " + |
| "but it does not even exist in the set of 'implicit' TransformerFactories", |
| implicit.remove(buggy)); |
| } |
| |
| implicit.removeAll(covered); |
| assertEquals("Some implicit TransformerFactories are not yet tested by this class: " + implicit, |
| 0, implicit.size()); |
| } |
| |
| |
| public void testRandomizedUpdatesAndRTGs() throws Exception { |
| |
| final int maxNumDocs = atLeast(100); |
| final int numSeedDocs = random().nextInt(maxNumDocs / 10); // at most ~10% of the max possible docs |
| final int numIters = atLeast(maxNumDocs * 10); |
| final SolrInputDocument[] knownDocs = new SolrInputDocument[maxNumDocs]; |
| |
| log.info("Starting {} iters by seeding {} of {} max docs", |
| numIters, numSeedDocs, maxNumDocs); |
| |
| int itersSinceLastCommit = 0; |
| for (int i = 0; i < numIters; i++) { |
| itersSinceLastCommit = maybeCommit(random(), itersSinceLastCommit, numIters); |
| |
| if (i < numSeedDocs) { |
| // first N iters all we worry about is seeding |
| knownDocs[i] = addRandomDocument(i); |
| } else { |
| assertOneIter(knownDocs); |
| } |
| } |
| } |
| |
| /** |
| * Randomly chooses to do a commit, where the probability of doing so increases the longer it's been since |
| * a commit was done. |
| * |
| * @returns <code>0</code> if a commit was done, else <code>itersSinceLastCommit + 1</code> |
| */ |
| private static int maybeCommit(final Random rand, final int itersSinceLastCommit, final int numIters) throws IOException, SolrServerException { |
| final float threshold = itersSinceLastCommit / numIters; |
| if (rand.nextFloat() < threshold) { |
| log.info("COMMIT"); |
| assertEquals(0, getRandClient(rand).commit().getStatus()); |
| return 0; |
| } |
| return itersSinceLastCommit + 1; |
| } |
| |
| private void assertOneIter(final SolrInputDocument[] knownDocs) throws IOException, SolrServerException { |
| // we want to occasionally test more then one doc per RTG |
| final int numDocsThisIter = TestUtil.nextInt(random(), 1, atLeast(2)); |
| int numDocsThisIterThatExist = 0; |
| |
| // pick some random docIds for this iteration and ... |
| final int[] docIds = new int[numDocsThisIter]; |
| for (int i = 0; i < numDocsThisIter; i++) { |
| docIds[i] = random().nextInt(knownDocs.length); |
| if (null != knownDocs[docIds[i]]) { |
| // ...check how many already exist |
| numDocsThisIterThatExist++; |
| } |
| } |
| |
| // we want our RTG requests to occasionally include missing/deleted docs, |
| // but that's not the primary focus of the test, so weight the odds accordingly |
| if (random().nextInt(numDocsThisIter + 2) <= numDocsThisIterThatExist) { |
| |
| if (0 < TestUtil.nextInt(random(), 0, 13)) { |
| log.info("RTG: numDocsThisIter={} numDocsThisIterThatExist={}, docIds={}", |
| numDocsThisIter, numDocsThisIterThatExist, docIds); |
| assertRTG(knownDocs, docIds); |
| } else { |
| // sporadically delete some docs instead of doing an RTG |
| log.info("DEL: numDocsThisIter={} numDocsThisIterThatExist={}, docIds={}", |
| numDocsThisIter, numDocsThisIterThatExist, docIds); |
| assertDelete(knownDocs, docIds); |
| } |
| } else { |
| log.info("UPD: numDocsThisIter={} numDocsThisIterThatExist={}, docIds={}", |
| numDocsThisIter, numDocsThisIterThatExist, docIds); |
| assertUpdate(knownDocs, docIds); |
| } |
| } |
| |
| /** |
| * Does some random indexing of the specified docIds and adds them to knownDocs |
| */ |
| private void assertUpdate(final SolrInputDocument[] knownDocs, final int[] docIds) throws IOException, SolrServerException { |
| |
| for (final int docId : docIds) { |
| // TODO: this method should also do some atomic update operations (ie: "inc" and "set") |
| // (but make sure to eval the updates locally as well before modifying knownDocs) |
| knownDocs[docId] = addRandomDocument(docId); |
| } |
| } |
| |
| /** |
| * Deletes the docIds specified and asserts the results are valid, updateing knownDocs accordingly |
| */ |
| private void assertDelete(final SolrInputDocument[] knownDocs, final int[] docIds) throws IOException, SolrServerException { |
| List<String> ids = new ArrayList<>(docIds.length); |
| for (final int docId : docIds) { |
| ids.add("" + docId); |
| knownDocs[docId] = null; |
| } |
| assertEquals("Failed delete: " + docIds, 0, getRandClient(random()).deleteById(ids).getStatus()); |
| } |
| |
| /** |
| * Adds one randomly generated document with the specified docId, asserting success, and returns |
| * the document added |
| */ |
| private SolrInputDocument addRandomDocument(final int docId) throws IOException, SolrServerException { |
| final SolrClient client = getRandClient(random()); |
| |
| final SolrInputDocument doc = sdoc("id", "" + docId, |
| "aaa_i", random().nextInt(), |
| "bbb_i", random().nextInt(), |
| // |
| "ccc_s", TestUtil.randomSimpleString(random()), |
| "ddd_s", TestUtil.randomSimpleString(random()), |
| "eee_s", TestUtil.randomSimpleString(random()), |
| "fff_s", TestUtil.randomSimpleString(random()), |
| "ggg_s", TestUtil.randomSimpleString(random()), |
| "hhh_s", TestUtil.randomSimpleString(random()), |
| // |
| "geo_1_srpt", GeoTransformerValidator.getValueForIndexing(random()), |
| "geo_2_srpt", GeoTransformerValidator.getValueForIndexing(random()), |
| // for testing subqueries |
| "next_2_ids_ss", String.valueOf(docId + 1), |
| "next_2_ids_ss", String.valueOf(docId + 2), |
| // for testing prefix globbing |
| "axx_i", random().nextInt(), |
| "ayy_i", random().nextInt(), |
| "azz_s", TestUtil.randomSimpleString(random())); |
| |
| log.info("ADD: {} = {}", docId, doc); |
| assertEquals(0, client.add(doc).getStatus()); |
| return doc; |
| } |
| |
| |
| /** |
| * Does one or more RTG request for the specified docIds with a randomized fl & fq params, asserting |
| * that the returned document (if any) makes sense given the expected SolrInputDocuments |
| */ |
| private void assertRTG(final SolrInputDocument[] knownDocs, final int[] docIds) throws IOException, SolrServerException { |
| final SolrClient client = getRandClient(random()); |
| // NOTE: not using SolrClient.getById or getByIds because we want to force choice of "id" vs "ids" params |
| final ModifiableSolrParams params = params("qt","/get"); |
| |
| // random fq -- nothing fancy, secondary concern for our test |
| final Integer FQ_MAX = usually() ? null : random().nextInt(); |
| if (null != FQ_MAX) { |
| params.add("fq", "aaa_i:[* TO " + FQ_MAX + "]"); |
| } |
| |
| final Set<FlValidator> validators = new LinkedHashSet<>(); |
| validators.add(ID_VALIDATOR); // always include id so we can be confident which doc we're looking at |
| validators.add(ROOT_VALIDATOR); // always added in a nested schema, with the same value as id |
| addRandomFlValidators(random(), validators); |
| FlValidator.addParams(validators, params); |
| |
| final List<String> idsToRequest = new ArrayList<>(docIds.length); |
| final List<SolrInputDocument> docsToExpect = new ArrayList<>(docIds.length); |
| for (int docId : docIds) { |
| // every docId will be included in the request |
| idsToRequest.add("" + docId); |
| |
| // only docs that should actually exist and match our (optional) filter will be expected in response |
| if (null != knownDocs[docId]) { |
| Integer filterVal = (Integer) knownDocs[docId].getFieldValue("aaa_i"); |
| if (null == FQ_MAX || ((null != filterVal) && filterVal.intValue() <= FQ_MAX.intValue())) { |
| docsToExpect.add(knownDocs[docId]); |
| } |
| } |
| } |
| |
| // even w/only 1 docId requested, the response format can vary depending on how we request it |
| final boolean askForList = random().nextBoolean() || (1 != idsToRequest.size()); |
| if (askForList) { |
| if (1 == idsToRequest.size()) { |
| // have to be careful not to try to use "multi" 'id' params with only 1 docId |
| // with a single docId, the only way to ask for a list is with the "ids" param |
| params.add("ids", idsToRequest.get(0)); |
| } else { |
| if (random().nextBoolean()) { |
| // each id in its own param |
| for (String id : idsToRequest) { |
| params.add("id",id); |
| } |
| } else { |
| // add one or more comma separated ids params |
| params.add(buildCommaSepParams(random(), "ids", idsToRequest)); |
| } |
| } |
| } else { |
| assert 1 == idsToRequest.size(); |
| params.add("id",idsToRequest.get(0)); |
| } |
| |
| final QueryResponse rsp = client.query(params); |
| assertNotNull(params.toString(), rsp); |
| |
| final SolrDocumentList docs = getDocsFromRTGResponse(askForList, rsp); |
| assertNotNull(params + " => " + rsp, docs); |
| |
| assertEquals("num docs mismatch: " + params + " => " + docsToExpect + " vs " + docs, |
| docsToExpect.size(), docs.size()); |
| |
| // NOTE: RTG makes no garuntees about the order docs will be returned in when multi requested |
| for (SolrDocument actual : docs) { |
| try { |
| int actualId = assertParseInt("id", actual.getFirstValue("id")); |
| final SolrInputDocument expected = knownDocs[actualId]; |
| assertNotNull("expected null doc but RTG returned: " + actual, expected); |
| |
| Set<String> expectedFieldNames = new TreeSet<>(); |
| for (FlValidator v : validators) { |
| expectedFieldNames.addAll(v.assertRTGResults(validators, expected, actual)); |
| } |
| // ensure only expected field names are in the actual document |
| Set<String> actualFieldNames = new TreeSet<>(actual.getFieldNames()); |
| assertEquals("Actual field names returned differs from expected", expectedFieldNames, actualFieldNames); |
| } catch (AssertionError ae) { |
| throw new AssertionError(params + " => " + actual + ": " + ae.getMessage(), ae); |
| } |
| } |
| } |
| |
| /** |
| * trivial helper method to deal with diff response structure between using a single 'id' param vs |
| * 2 or more 'id' params (or 1 or more 'ids' params). |
| * |
| * @return List from response, or a synthetic one created from single response doc if |
| * <code>expectList</code> was false; May be empty; May be null if response included null list. |
| */ |
| private static SolrDocumentList getDocsFromRTGResponse(final boolean expectList, final QueryResponse rsp) { |
| if (expectList) { |
| return rsp.getResults(); |
| } |
| |
| // else: expect single doc, make our own list... |
| |
| final SolrDocumentList result = new SolrDocumentList(); |
| NamedList<Object> raw = rsp.getResponse(); |
| Object doc = raw.get("doc"); |
| if (null != doc) { |
| result.add((SolrDocument) doc); |
| result.setNumFound(1); |
| } |
| return result; |
| } |
| |
| /** |
| * returns a random SolrClient -- either a CloudSolrClient, or an HttpSolrClient pointed |
| * at a node in our cluster |
| */ |
| public static SolrClient getRandClient(Random rand) { |
| int numClients = CLIENTS.size(); |
| int idx = TestUtil.nextInt(rand, 0, numClients); |
| return (idx == numClients) ? CLOUD_CLIENT : CLIENTS.get(idx); |
| } |
| |
| public static void waitForRecoveriesToFinish(CloudSolrClient client) throws Exception { |
| assert null != client.getDefaultCollection(); |
| AbstractDistribZkTestBase.waitForRecoveriesToFinish(client.getDefaultCollection(), |
| client.getZkStateReader(), |
| true, true, 330); |
| } |
| |
| /** |
| * Abstraction for diff types of things that can be added to an 'fl' param that can validate |
| * the results are correct compared to an expected SolrInputDocument |
| */ |
| private interface FlValidator { |
| |
| /** |
| * Given a list of FlValidators, adds one or more fl params that corrispond to the entire set, |
| * as well as any other special case top level params required by the validators. |
| */ |
| public static void addParams(final Collection<FlValidator> validators, final ModifiableSolrParams params) { |
| final List<String> fls = new ArrayList<>(validators.size()); |
| for (FlValidator v : validators) { |
| params.add(v.getExtraRequestParams()); |
| fls.add(v.getFlParam()); |
| } |
| params.add(buildCommaSepParams(random(), "fl", fls)); |
| } |
| |
| /** |
| * Indicates if this validator is for a transformer that returns true from |
| * {@link DocTransformer#needsSolrIndexSearcher}. Other validators for transformers that |
| * do <em>not</em> require a re-opened searcher (but may have slightly diff behavior depending |
| * on wether a doc comesfrom the index or from the update log) may use this information to |
| * decide wether they wish to enforce stricter assertions on the resulting document. |
| * |
| * The default implementation always returns <code>false</code> |
| * |
| * @see DocIdValidator |
| */ |
| public default boolean requiresRealtimeSearcherReOpen() { |
| return false; |
| } |
| |
| /** |
| * the name of a transformer listed in {@link TransformerFactory#defaultFactories} that this validator |
| * corrisponds to, or null if not applicable. Used for testing coverage of |
| * Solr's implicitly supported transformers. |
| * |
| * Default behavior is to return null |
| * @see #testCoverage |
| */ |
| public default String getDefaultTransformerFactoryName() { return null; } |
| |
| /** |
| * Any special case params that must be added to the request for this validator |
| */ |
| public default SolrParams getExtraRequestParams() { return params(); } |
| |
| /** |
| * Must return a non null String that can be used in an fl param -- either by itself, |
| * or with other items separated by commas |
| */ |
| public String getFlParam(); |
| |
| /** |
| * Given the expected document and the actual document returned from an RTG, this method |
| * should assert that relative to what {@link #getFlParam} returns, the actual document contained |
| * what it should relative to the expected document. |
| * |
| * @param validators all validators in use for this request, including the current one |
| * @param expected a document containing the expected fields & values that should be in the index |
| * @param actual A document that was returned by an RTG request |
| * @return A set of "field names" in the actual document that this validator expected. |
| */ |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual); |
| } |
| |
| /** |
| * Some validators behave in a way that "suppresses" real fields even when they would otherwise match a glob |
| * @see GlobValidator |
| */ |
| private interface SuppressRealFields { |
| public Set<String> getSuppressedFields(); |
| } |
| |
| private abstract static class FieldValueValidator implements FlValidator { |
| protected final String expectedFieldName; |
| protected final String actualFieldName; |
| public FieldValueValidator(final String expectedFieldName, final String actualFieldName) { |
| this.expectedFieldName = expectedFieldName; |
| this.actualFieldName = actualFieldName; |
| } |
| public abstract String getFlParam(); |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| assertEquals(expectedFieldName + " vs " + actualFieldName, |
| expected.getFieldValue(expectedFieldName), actual.getFirstValue(actualFieldName)); |
| return Collections.<String>singleton(actualFieldName); |
| } |
| } |
| |
| private static class SimpleFieldValueValidator extends FieldValueValidator { |
| public SimpleFieldValueValidator(final String fieldName) { |
| super(fieldName, fieldName); |
| } |
| public String getFlParam() { return expectedFieldName; } |
| } |
| |
| private static class RenameFieldValueValidator extends FieldValueValidator implements SuppressRealFields { |
| public RenameFieldValueValidator(final String origFieldName, final String alias) { |
| super(origFieldName, alias); |
| } |
| public String getFlParam() { return actualFieldName + ":" + expectedFieldName; } |
| public Set<String> getSuppressedFields() { return Collections.singleton(expectedFieldName); } |
| } |
| |
| /** |
| * Validator for {@link RawValueTransformerFactory} |
| * |
| * This validator is fairly weak, because it doesn't do anything to verify the conditional logic |
| * in RawValueTransformerFactory realted to the output format -- but that's out of the scope of |
| * this randomized testing. |
| * |
| * What we're primarily concerned with is that the transformer does it's job and puts the string |
| * in the response, regardless of cloud/RTG/uncommited state of the document. |
| */ |
| private static class RawFieldValueValidator extends RenameFieldValueValidator { |
| final String type; |
| final String alias; |
| public RawFieldValueValidator(final String type, final String fieldName, final String alias) { |
| // transformer is weird, default result key doesn't care what params are used... |
| super(fieldName, null == alias ? "["+type+"]" : alias); |
| this.type = type; |
| this.alias = alias; |
| } |
| public RawFieldValueValidator(final String type, final String fieldName) { |
| this(type, fieldName, null); |
| } |
| public String getFlParam() { |
| return (null == alias ? "" : (alias + ":")) + "[" + type + " f=" + expectedFieldName + "]"; |
| } |
| public String getDefaultTransformerFactoryName() { |
| return type; |
| } |
| } |
| |
| |
| /** |
| * enforces that a valid <code>[docid]</code> is present in the response, possibly using a |
| * resultKey alias. By default the only validation of docId values is that they are an integer |
| * greater than or equal to <code>-1</code> -- but if any other validator in use returns true |
| * from {@link #requiresRealtimeSearcherReOpen} then the constraint is tightened and values must |
| * be greater than or equal to <code>0</code> |
| */ |
| private static class DocIdValidator implements FlValidator { |
| private static final String NAME = "docid"; |
| private static final String USAGE = "["+NAME+"]"; |
| private final String resultKey; |
| public DocIdValidator(final String resultKey) { |
| this.resultKey = resultKey; |
| } |
| public DocIdValidator() { |
| this(USAGE); |
| } |
| public String getDefaultTransformerFactoryName() { return NAME; } |
| public String getFlParam() { return USAGE.equals(resultKey) ? resultKey : resultKey+":"+USAGE; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| final Object value = actual.getFirstValue(resultKey); |
| assertNotNull(getFlParam() + " => no value in actual doc", value); |
| assertTrue(USAGE + " must be an Integer: " + value, value instanceof Integer); |
| |
| int minValidDocId = -1; // if it comes from update log |
| for (FlValidator other : validators) { |
| if (other.requiresRealtimeSearcherReOpen()) { |
| minValidDocId = 0; |
| break; |
| } |
| } |
| assertTrue(USAGE + " must be >= " + minValidDocId + ": " + value, |
| minValidDocId <= ((Integer)value).intValue()); |
| return Collections.<String>singleton(resultKey); |
| } |
| } |
| |
| /** Trivial validator of ShardAugmenterFactory */ |
| private static class ShardValidator implements FlValidator { |
| private static final String NAME = "shard"; |
| private static final String USAGE = "["+NAME+"]"; |
| private final String resultKey; |
| public ShardValidator(final String resultKey) { |
| this.resultKey = resultKey; |
| } |
| public ShardValidator() { |
| this(USAGE); |
| } |
| public String getDefaultTransformerFactoryName() { return NAME; } |
| public String getFlParam() { return USAGE.equals(resultKey) ? resultKey : resultKey+":"+USAGE; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| final Object value = actual.getFirstValue(resultKey); |
| assertNotNull(getFlParam() + " => no value in actual doc", value); |
| assertTrue(USAGE + " must be an String: " + value, value instanceof String); |
| |
| // trivial sanity check |
| assertFalse(USAGE + " => blank string", value.toString().trim().isEmpty()); |
| return Collections.<String>singleton(resultKey); |
| } |
| } |
| |
| /** Trivial validator of ValueAugmenter */ |
| private static class ValueAugmenterValidator implements FlValidator { |
| private static final String NAME = "value"; |
| private static String trans(final int value) { return "[" + NAME + " v=" + value + " t=int]"; } |
| |
| private final String resultKey; |
| private final String fl; |
| private final Integer expectedVal; |
| private ValueAugmenterValidator(final String fl, final int expectedVal, final String resultKey) { |
| this.resultKey = resultKey; |
| this.expectedVal = expectedVal; |
| this.fl = fl; |
| } |
| public ValueAugmenterValidator(final int expectedVal, final String resultKey) { |
| this(resultKey + ":" +trans(expectedVal), expectedVal, resultKey); |
| } |
| public ValueAugmenterValidator(final int expectedVal) { |
| // value transformer is weird, default result key doesn't care what params are used... |
| this(trans(expectedVal), expectedVal, "["+NAME+"]"); |
| } |
| public String getDefaultTransformerFactoryName() { return NAME; } |
| public String getFlParam() { return fl; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| final Object actualVal = actual.getFirstValue(resultKey); |
| assertNotNull(getFlParam() + " => no value in actual doc", actualVal); |
| assertEquals(getFlParam(), expectedVal, actualVal); |
| return Collections.<String>singleton(resultKey); |
| } |
| } |
| |
| |
| /** Trivial validator of a ValueSourceAugmenter */ |
| private static class FunctionValidator implements FlValidator { |
| private static String func(String fieldName) { |
| return "add(1.3,sub("+fieldName+","+fieldName+"))"; |
| } |
| protected final String fl; |
| protected final String resultKey; |
| protected final String fieldName; |
| public FunctionValidator(final String fieldName) { |
| this(func(fieldName), fieldName, func(fieldName)); |
| } |
| public FunctionValidator(final String fieldName, final String resultKey) { |
| this(resultKey + ":" + func(fieldName), fieldName, resultKey); |
| } |
| private FunctionValidator(final String fl, final String fieldName, final String resultKey) { |
| this.fl = fl; |
| this.resultKey = resultKey; |
| this.fieldName = fieldName; |
| } |
| /** always returns true */ |
| public boolean requiresRealtimeSearcherReOpen() { return true; } |
| public String getFlParam() { return fl; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| final Object origVal = expected.getFieldValue(fieldName); |
| assertTrue("this validator only works on numeric fields: " + origVal, origVal instanceof Number); |
| |
| assertEquals(fl, 1.3F, actual.getFirstValue(resultKey)); |
| return Collections.<String>singleton(resultKey); |
| } |
| } |
| |
| |
| /** |
| * Trivial validator of a SubQueryAugmenter. |
| * |
| * This validator ignores 90% of the features/complexity |
| * of SubQueryAugmenter, and instead just focuses on the basics of: |
| * <ul> |
| * <li>do a subquery for docs where SUBQ_FIELD contains the id of the top level doc</li> |
| * <li>verify that any subquery match is expected based on indexing pattern</li> |
| * </ul> |
| */ |
| private static class SubQueryValidator implements FlValidator { |
| |
| // HACK to work around SOLR-9396... |
| // |
| // we're using "id" (and only "id") in the subquery.q as a workarround limitation in |
| // "$rows.foo" parsing -- it only works reliably if "foo" is in fl, so we only use "$rows.id", |
| // which we know is in every request (and is a valid integer) |
| |
| public final static String NAME = "subquery"; |
| public final static String SUBQ_KEY = "subq"; |
| public final static String SUBQ_FIELD = "next_2_ids_i"; |
| public String getFlParam() { return SUBQ_KEY+":["+NAME+"]"; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| final int compVal = assertParseInt("expected id", expected.getFieldValue("id")); |
| |
| final Object actualVal = actual.getFieldValue(SUBQ_KEY); |
| assertTrue("Expected a doclist: " + actualVal, |
| actualVal instanceof SolrDocumentList); |
| assertTrue("should be at most 2 docs in doc list: " + actualVal, |
| ((SolrDocumentList) actualVal).getNumFound() <= 2); |
| |
| for (SolrDocument subDoc : (SolrDocumentList) actualVal) { |
| final int subDocIdVal = assertParseInt("subquery id", subDoc.getFirstValue("id")); |
| assertTrue("subDocId="+subDocIdVal+" not in valid range for id="+compVal+" (expected " |
| + (compVal-1) + " or " + (compVal-2) + ")", |
| ((subDocIdVal < compVal) && ((compVal-2) <= subDocIdVal))); |
| |
| } |
| |
| return Collections.<String>singleton(SUBQ_KEY); |
| } |
| public String getDefaultTransformerFactoryName() { return NAME; } |
| public SolrParams getExtraRequestParams() { |
| return params(SubQueryValidator.SUBQ_KEY + ".q", |
| "{!field f=" + SubQueryValidator.SUBQ_FIELD + " v=$row.id}"); |
| } |
| } |
| |
| /** Trivial validator of a GeoTransformer */ |
| private static class GeoTransformerValidator implements FlValidator, SuppressRealFields{ |
| private static final String NAME = "geo"; |
| /** |
| * we're not worried about testing the actual geo parsing/formatting of values, |
| * just that the transformer gets called with the expected field value. |
| * so have a small set of fixed input values we use when indexing docs, |
| * and the expected output for each |
| */ |
| private static final Map<String,String> VALUES = new HashMap<>(); |
| /** |
| * The set of legal field values this validator is willing to test as a list so we can |
| * reliably index into it with random ints |
| */ |
| private static final List<String> ALLOWED_FIELD_VALUES; |
| static { |
| for (int i = -42; i < 66; i+=13) { |
| VALUES.put("POINT( 42 "+i+" )", "{\"type\":\"Point\",\"coordinates\":[42,"+i+"]}"); |
| } |
| ALLOWED_FIELD_VALUES = Collections.unmodifiableList(new ArrayList<>(VALUES.keySet())); |
| } |
| /** |
| * returns a random field value usable when indexing a document that this validator will |
| * be able to handle. |
| */ |
| public static String getValueForIndexing(final Random rand) { |
| return ALLOWED_FIELD_VALUES.get(rand.nextInt(ALLOWED_FIELD_VALUES.size())); |
| } |
| private static String trans(String fieldName) { |
| return "["+NAME+" f="+fieldName+"]"; |
| } |
| protected final String fl; |
| protected final String resultKey; |
| protected final String fieldName; |
| public GeoTransformerValidator(final String fieldName) { |
| // geo transformer is weird, default result key doesn't care what params are used... |
| this(trans(fieldName), fieldName, "["+NAME+"]"); |
| } |
| public GeoTransformerValidator(final String fieldName, final String resultKey) { |
| this(resultKey + ":" + trans(fieldName), fieldName, resultKey); |
| } |
| private GeoTransformerValidator(final String fl, final String fieldName, final String resultKey) { |
| this.fl = fl; |
| this.resultKey = resultKey; |
| this.fieldName = fieldName; |
| } |
| public String getDefaultTransformerFactoryName() { return NAME; } |
| public String getFlParam() { return fl; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| final Object origVal = expected.getFieldValue(fieldName); |
| assertTrue(fl + ": orig field value is not supported: " + origVal, VALUES.containsKey(origVal)); |
| |
| assertEquals(fl, VALUES.get(origVal), actual.getFirstValue(resultKey)); |
| return Collections.<String>singleton(resultKey); |
| } |
| public Set<String> getSuppressedFields() { return Collections.singleton(fieldName); } |
| } |
| |
| /** |
| * Glob based validator. |
| * This class checks that every field in the expected doc exists in the actual doc with the expected |
| * value -- with special exceptions for fields that are "suppressed" (usually via an alias) |
| * |
| * By design, fields that are aliased are "moved" unless the original field name was explicitly included |
| * in the fl, globs don't count. |
| * |
| * @see RenameFieldValueValidator |
| */ |
| private static class GlobValidator implements FlValidator { |
| private final String glob; |
| public GlobValidator(final String glob) { |
| this.glob = glob; |
| } |
| private final Set<String> matchingFieldsCache = new LinkedHashSet<>(); |
| |
| public String getFlParam() { return glob; } |
| |
| private boolean matchesGlob(final String fieldName) { |
| if ( FilenameUtils.wildcardMatch(fieldName, glob) ) { |
| matchingFieldsCache.add(fieldName); // Don't calculate it again |
| return true; |
| } |
| return false; |
| } |
| |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| |
| final Set<String> renamed = new LinkedHashSet<>(validators.size()); |
| for (FlValidator v : validators) { |
| if (v instanceof SuppressRealFields) { |
| renamed.addAll(((SuppressRealFields)v).getSuppressedFields()); |
| } |
| } |
| |
| // every real field name matching the glob that is not renamed should be in the results |
| Set<String> result = new LinkedHashSet<>(expected.getFieldNames().size()); |
| for (String f : expected.getFieldNames()) { |
| if ( matchesGlob(f) && (! renamed.contains(f) ) ) { |
| result.add(f); |
| assertEquals(glob + " => " + f, expected.getFieldValue(f), actual.getFirstValue(f)); |
| } |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * for things like "score" and "[explain]" where we explicitly expect what we ask for in the fl |
| * to <b>not</b> be returned when using RTG. |
| */ |
| private static class NotIncludedValidator implements FlValidator { |
| private final String fieldName; |
| private final String fl; |
| public NotIncludedValidator(final String fl) { |
| this(fl, fl); |
| } |
| public NotIncludedValidator(final String fieldName, final String fl) { |
| this.fieldName = fieldName; |
| this.fl = fl; |
| } |
| public String getFlParam() { return fl; } |
| public Collection<String> assertRTGResults(final Collection<FlValidator> validators, |
| final SolrInputDocument expected, |
| final SolrDocument actual) { |
| assertEquals(fl, null, actual.getFirstValue(fieldName)); |
| return Collections.emptySet(); |
| } |
| } |
| |
| /** explain should always be ignored when using RTG */ |
| private static class ExplainValidator extends NotIncludedValidator { |
| private final static String NAME = "explain"; |
| private final static String USAGE = "[" + NAME + "]"; |
| public ExplainValidator() { |
| super(USAGE); |
| } |
| public ExplainValidator(final String resultKey) { |
| super(USAGE, resultKey + ":" + USAGE); |
| } |
| public String getDefaultTransformerFactoryName() { return NAME; } |
| } |
| |
| /** helper method for adding a random number (may be 0) of items from {@link #FL_VALIDATORS} */ |
| private static void addRandomFlValidators(final Random r, final Set<FlValidator> validators) { |
| List<FlValidator> copyToShuffle = new ArrayList<>(FL_VALIDATORS); |
| Collections.shuffle(copyToShuffle, r); |
| final int numToReturn = r.nextInt(copyToShuffle.size()); |
| validators.addAll(copyToShuffle.subList(0, numToReturn + 1)); |
| } |
| |
| /** |
| * Given an ordered list of values to include in a (key) param, randomly groups them (ie: comma separated) |
| * into actual param key=values which are returned as a new SolrParams instance |
| */ |
| private static SolrParams buildCommaSepParams(final Random rand, final String key, Collection<String> values) { |
| ModifiableSolrParams result = new ModifiableSolrParams(); |
| List<String> copy = new ArrayList<>(values); |
| while (! copy.isEmpty()) { |
| List<String> slice = copy.subList(0, random().nextInt(1 + copy.size())); |
| result.add(key,String.join(",",slice)); |
| slice.clear(); |
| } |
| return result; |
| } |
| |
| /** helper method for asserting an object is a non-null String can be parsed as an int */ |
| public static int assertParseInt(String msg, Object orig) { |
| assertNotNull(msg + ": is null", orig); |
| assertTrue(msg + ": is not a string: " + orig, orig instanceof String); |
| try { |
| return Integer.parseInt(orig.toString()); |
| } catch (NumberFormatException nfe) { |
| throw new AssertionError(msg + ": can't be parsed as a number: " + orig, nfe); |
| } |
| } |
| } |