blob: b20f3dfd14d5cdc9439fc8240ee0536f55486d99 [file] [log] [blame]
/*
* 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 &amp; 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 &amp; 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);
}
}
}