blob: c4ae8d753c8e5e7f09f19f7c4933465a531a18d8 [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;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.Filter;
import org.apache.commons.io.FileUtils;
import org.apache.lucene.util.Constants;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.JettyConfig;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.UpdateResponse;
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.ExecutorUtil;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import junit.framework.Assert;
/**
* Helper base class for distributed search test cases
*
* By default, all tests in sub-classes will be executed with
* 1, 2, ... DEFAULT_MAX_SHARD_COUNT number of shards set up repeatedly.
*
* In general, it's preferable to annotate the tests in sub-classes with a
* {@literal @}ShardsFixed(num = N) or a {@literal @}ShardsRepeat(min = M, max = N)
* to indicate whether the test should be called once, with a fixed number of shards,
* or called repeatedly for number of shards = M to N.
*
* In some cases though, if the number of shards has to be fixed, but the number
* itself is dynamic, or if it has to be set as a default for all sub-classes
* of a sub-class, there's a fixShardCount(N) available, which is identical to
* {@literal @}ShardsFixed(num = N) for all tests without annotations in that class
* hierarchy. Ideally this function should be retired in favour of better annotations..
*
* @since solr 1.5
*/
public abstract class BaseDistributedSearchTestCase extends SolrTestCaseJ4 {
protected ExecutorService executor = new ExecutorUtil.MDCAwareThreadPoolExecutor(
4,
Integer.MAX_VALUE,
15, TimeUnit.SECONDS, // terminate idle threads after 15 sec
new SynchronousQueue<>(), // directly hand off tasks
new SolrNamedThreadFactory("BaseDistributedSearchTestCase"),
false
);
// TODO: this shouldn't be static. get the random when you need it to avoid sharing.
public static Random r;
private AtomicInteger nodeCnt = new AtomicInteger(0);
protected boolean useExplicitNodeNames;
@BeforeClass
public static void initialize() {
assumeFalse("SOLR-4147: ibm 64bit has jvm bugs!", Constants.JRE_IS_64BIT && Constants.JAVA_VENDOR.startsWith("IBM"));
r = new Random(random().nextLong());
}
/**
* Set's the value of the "hostContext" system property to a random path
* like string (which may or may not contain sub-paths). This is used
* in the default constructor for this test to help ensure no code paths have
* hardcoded assumptions about the servlet context used to run solr.
* <p>
* Test configs may use the <code>${hostContext}</code> variable to access
* this system property.
* </p>
* @see #BaseDistributedSearchTestCase()
* @see #clearHostContext
*/
@BeforeClass
public static void initHostContext() {
// Can't use randomRealisticUnicodeString because unescaped unicode is
// not allowed in URL paths
// Can't use URLEncoder.encode(randomRealisticUnicodeString) because
// Jetty freaks out and returns 404's when the context uses escapes
StringBuilder hostContext = new StringBuilder("/");
if (random().nextBoolean()) {
// half the time we use the root context, the other half...
// Remember: randomSimpleString might be the empty string
hostContext.append(TestUtil.randomSimpleString(random(), 2));
if (random().nextBoolean()) {
hostContext.append("_");
}
hostContext.append(TestUtil.randomSimpleString(random(), 3));
if ( ! "/".equals(hostContext.toString())) {
// if our random string is empty, this might add a trailing slash,
// but our code should be ok with that
hostContext.append("/").append(TestUtil.randomSimpleString(random(), 2));
} else {
// we got 'lucky' and still just have the root context,
// NOOP: don't try to add a subdir to nothing (ie "//" is bad)
}
}
// paranoia, we *really* don't want to ever get "//" in a path...
final String hc = hostContext.toString().replaceAll("\\/+","/");
log.info("Setting hostContext system property: {}", hc);
System.setProperty("hostContext", hc);
}
/**
* Clears the "hostContext" system property
* @see #initHostContext
*/
@AfterClass
public static void clearHostContext() throws Exception {
System.clearProperty("hostContext");
}
@SuppressWarnings("deprecation")
@BeforeClass
public static void setSolrDisableShardsWhitelist() throws Exception {
systemSetPropertySolrDisableShardsWhitelist("true");
}
@SuppressWarnings("deprecation")
@AfterClass
public static void clearSolrDisableShardsWhitelist() throws Exception {
systemClearPropertySolrDisableShardsWhitelist();
}
private static String getHostContextSuitableForServletContext() {
String ctx = System.getProperty("hostContext","/solr");
if ("".equals(ctx)) ctx = "/solr";
if (ctx.endsWith("/")) ctx = ctx.substring(0,ctx.length()-1);;
if (!ctx.startsWith("/")) ctx = "/" + ctx;
return ctx;
}
/**
* Constructs a test in which the jetty+solr instances as well as the
* solr clients all use the value of the "hostContext" system property.
* <p>
* If the system property is not set, or is set to the empty string
* (neither of which should normally happen unless a subclass explicitly
* modifies the property set by {@link #initHostContext} prior to calling
* this constructor) a servlet context of "/solr" is used. (this is for
* consistency with the default behavior of solr.xml parsing when using
* <code>hostContext="${hostContext:}"</code>
* </p>
* <p>
* If the system property is set to a value which does not begin with a
* "/" (which should normally happen unless a subclass explicitly
* modifies the property set by {@link #initHostContext} prior to calling
* this constructor) a leading "/" will be prepended.
* </p>
*
* @see #initHostContext
*/
protected BaseDistributedSearchTestCase() {
this(getHostContextSuitableForServletContext());
}
/**
* @param context explicit servlet context path to use (eg: "/solr")
*/
protected BaseDistributedSearchTestCase(final String context) {
this.context = context;
this.deadServers = new String[] {DEAD_HOST_1 + context,
DEAD_HOST_2 + context,
DEAD_HOST_3 + context};
}
private final static int DEFAULT_MAX_SHARD_COUNT = 3;
private int shardCount = -1; // the actual number of solr cores that will be created in the cluster
public int getShardCount() {
return shardCount;
}
private boolean isShardCountFixed = false;
public void fixShardCount(int count) {
isShardCountFixed = true;
shardCount = count;
}
protected volatile JettySolrRunner controlJetty;
protected final List<SolrClient> clients = Collections.synchronizedList(new ArrayList<>());
protected final List<JettySolrRunner> jettys = Collections.synchronizedList(new ArrayList<>());
protected volatile String context;
protected volatile String[] deadServers;
protected volatile String shards;
protected volatile String[] shardsArr;
protected volatile File testDir;
protected volatile SolrClient controlClient;
// to stress with higher thread counts and requests, make sure the junit
// xml formatter is not being used (all output will be buffered before
// transformation to xml and cause an OOM exception).
protected volatile int stress = TEST_NIGHTLY ? 2 : 0;
protected volatile boolean verifyStress = true;
protected volatile int nThreads = 3;
public final static int ORDERED = 1;
public final static int SKIP = 2;
public final static int SKIPVAL = 4;
public final static int UNORDERED = 8;
/**
* When this flag is set, Double values will be allowed a difference ratio of 1E-8
* between the non-distributed and the distributed returned values
*/
public static int FUZZY = 16;
private static final double DOUBLE_RATIO_LIMIT = 1E-8;
protected volatile int flags;
protected Map<String, Integer> handle = new ConcurrentHashMap<>();
protected String id = "id";
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static RandVal rint = new RandVal() {
@Override
public Object val() {
return r.nextInt();
}
};
public static RandVal rlong = new RandVal() {
@Override
public Object val() {
return r.nextLong();
}
};
public static RandVal rfloat = new RandVal() {
@Override
public Object val() {
return r.nextFloat();
}
};
public static RandVal rdouble = new RandVal() {
@Override
public Object val() {
return r.nextDouble();
}
};
public static RandVal rdate = new RandDate();
public static String[] fieldNames = new String[]{"n_ti1", "n_f1", "n_tf1", "n_d1", "n_td1", "n_l1", "n_tl1", "n_dt1", "n_tdt1"};
public static RandVal[] randVals = new RandVal[]{rint, rfloat, rfloat, rdouble, rdouble, rlong, rlong, rdate, rdate};
protected String[] getFieldNames() {
return fieldNames;
}
protected RandVal[] getRandValues() {
return randVals;
}
/**
* Subclasses can override this to change a test's solr home
* (default is in test-files)
*/
public String getSolrHome() {
return SolrTestCaseJ4.TEST_HOME();
}
private boolean distribSetUpCalled = false;
public void distribSetUp() throws Exception {
distribSetUpCalled = true;
SolrTestCaseJ4.resetExceptionIgnores(); // ignore anything with ignore_exception in it
System.setProperty("solr.test.sys.prop1", "propone");
System.setProperty("solr.test.sys.prop2", "proptwo");
testDir = createTempDir().toFile();
}
private volatile boolean distribTearDownCalled = false;
public void distribTearDown() throws Exception {
ExecutorUtil.shutdownAndAwaitTermination(executor);
distribTearDownCalled = true;
}
protected JettySolrRunner createControlJetty() throws Exception {
Path jettyHome = testDir.toPath().resolve("control");
File jettyHomeFile = jettyHome.toFile();
seedSolrHome(jettyHomeFile);
seedCoreRootDirWithDefaultTestCore(jettyHome.resolve("cores"));
JettySolrRunner jetty = createJetty(jettyHomeFile, null, null, getSolrConfigFile(), getSchemaFile());
jetty.start();
return jetty;
}
protected void createServers(int numShards) throws Exception {
System.setProperty("configSetBaseDir", getSolrHome());
controlJetty = createControlJetty();
controlClient = createNewSolrClient(controlJetty.getLocalPort());
shardsArr = new String[numShards];
StringBuilder sb = new StringBuilder();
for (int i = 0; i < numShards; i++) {
if (sb.length() > 0) sb.append(',');
final String shardname = "shard" + i;
Path jettyHome = testDir.toPath().resolve(shardname);
File jettyHomeFile = jettyHome.toFile();
seedSolrHome(jettyHomeFile);
seedCoreRootDirWithDefaultTestCore(jettyHome.resolve("cores"));
JettySolrRunner j = createJetty(jettyHomeFile, null, null, getSolrConfigFile(), getSchemaFile());
j.start();
jettys.add(j);
clients.add(createNewSolrClient(j.getLocalPort()));
String shardStr = buildUrl(j.getLocalPort());
if (shardStr.endsWith("/")) shardStr += DEFAULT_TEST_CORENAME;
else shardStr += "/" + DEFAULT_TEST_CORENAME;
shardsArr[i] = shardStr;
sb.append(shardStr);
}
shards = sb.toString();
}
protected void setDistributedParams(ModifiableSolrParams params) {
params.set("shards", getShardsString());
}
protected String getShardsString() {
if (deadServers == null) return shards;
StringBuilder sb = new StringBuilder();
for (String shard : shardsArr) {
if (sb.length() > 0) sb.append(',');
int nDeadServers = r.nextInt(deadServers.length+1);
if (nDeadServers > 0) {
List<String> replicas = new ArrayList<>(Arrays.asList(deadServers));
Collections.shuffle(replicas, r);
replicas.add(r.nextInt(nDeadServers+1), shard);
for (int i=0; i<nDeadServers+1; i++) {
if (i!=0) sb.append('|');
sb.append(replicas.get(i));
}
} else {
sb.append(shard);
}
}
return sb.toString();
}
protected void destroyServers() throws Exception {
ExecutorService customThreadPool = ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("closeThreadPool"));
customThreadPool.submit(() -> IOUtils.closeQuietly(controlClient));
customThreadPool.submit(() -> {
try {
controlJetty.stop();
} catch (NullPointerException e) {
// ignore
} catch (Exception e) {
log.error("Error stopping Control Jetty", e);
}
});
for (SolrClient client : clients) {
customThreadPool.submit(() -> IOUtils.closeQuietly(client));
}
for (JettySolrRunner jetty : jettys) {
customThreadPool.submit(() -> {
try {
jetty.stop();
} catch (Exception e) {
log.error("Error stopping Jetty", e);
}
});
}
ExecutorUtil.shutdownAndAwaitTermination(customThreadPool);
clients.clear();
jettys.clear();
}
public JettySolrRunner createJetty(File solrHome, String dataDir) throws Exception {
return createJetty(solrHome, dataDir, null, null, null);
}
public JettySolrRunner createJetty(File solrHome, String dataDir, String shardId) throws Exception {
return createJetty(solrHome, dataDir, shardId, null, null);
}
public JettySolrRunner createJetty(File solrHome, String dataDir, String shardList, String solrConfigOverride, String schemaOverride) throws Exception {
return createJetty(solrHome, dataDir, shardList, solrConfigOverride, schemaOverride, useExplicitNodeNames);
}
public JettySolrRunner createJetty(File solrHome, String dataDir, String shardList, String solrConfigOverride, String schemaOverride, boolean explicitCoreNodeName) throws Exception {
Properties props = new Properties();
if (solrConfigOverride != null)
props.setProperty("solrconfig", solrConfigOverride);
if (schemaOverride != null)
props.setProperty("schema", schemaOverride);
if (shardList != null)
props.setProperty("shards", shardList);
if (dataDir != null) {
props.setProperty("solr.data.dir", dataDir);
}
if (explicitCoreNodeName) {
props.setProperty("coreNodeName", Integer.toString(nodeCnt.incrementAndGet()));
}
props.setProperty("coreRootDirectory", solrHome.toPath().resolve("cores").toAbsolutePath().toString());
JettySolrRunner jetty = new JettySolrRunner(solrHome.getAbsolutePath(), props, JettyConfig.builder()
.stopAtShutdown(true)
.setContext(context)
.withFilters(getExtraRequestFilters())
.withServlets(getExtraServlets())
.withSSLConfig(sslConfig.buildServerSSLConfig())
.build());
return jetty;
}
/** Override this method to insert extra servlets into the JettySolrRunners that are created using createJetty() */
public SortedMap<ServletHolder,String> getExtraServlets() {
return null;
}
/** Override this method to insert extra filters into the JettySolrRunners that are created using createJetty() */
public SortedMap<Class<? extends Filter>,String> getExtraRequestFilters() {
return null;
}
protected SolrClient createNewSolrClient(int port) {
try {
// setup the client...
String baseUrl = buildUrl(port);
if (baseUrl.endsWith("/")) {
return getHttpSolrClient(baseUrl + DEFAULT_TEST_CORENAME);
} else {
return getHttpSolrClient(baseUrl + "/" + DEFAULT_TEST_CORENAME);
}
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
protected String buildUrl(int port) {
return buildUrl(port, context);
}
protected static void addFields(SolrInputDocument doc, Object... fields) {
for (int i = 0; i < fields.length; i += 2) {
doc.addField((String) (fields[i]), fields[i + 1]);
}
}// add random fields to the documet before indexing
protected void indexr(Object... fields) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
addFields(doc, fields);
addFields(doc, "rnd_b", true);
addRandFields(doc);
indexDoc(doc);
}
protected SolrInputDocument addRandFields(SolrInputDocument sdoc) {
addFields(sdoc, getRandFields(getFieldNames(), getRandValues()));
return sdoc;
}
protected void index(Object... fields) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
addFields(doc, fields);
indexDoc(doc);
}
/**
* Indexes the document in both the control client, and a randomly selected client
*/
protected void indexDoc(SolrInputDocument doc) throws IOException, SolrServerException {
controlClient.add(doc);
if (shardCount == 0) {//mostly for temp debugging
return;
}
int which = (doc.getField(id).toString().hashCode() & 0x7fffffff) % clients.size();
SolrClient client = clients.get(which);
client.add(doc);
}
/**
* Indexes the document in both the control client and the specified client asserting
* that the responses are equivalent
*/
protected UpdateResponse indexDoc(SolrClient client, SolrParams params, SolrInputDocument... sdocs) throws IOException, SolrServerException {
UpdateResponse controlRsp = add(controlClient, params, sdocs);
UpdateResponse specificRsp = add(client, params, sdocs);
compareSolrResponses(specificRsp, controlRsp);
return specificRsp;
}
protected UpdateResponse add(SolrClient client, SolrParams params, SolrInputDocument... sdocs) throws IOException, SolrServerException {
UpdateRequest ureq = new UpdateRequest();
ureq.setParams(new ModifiableSolrParams(params));
for (SolrInputDocument sdoc : sdocs) {
ureq.add(sdoc);
}
return ureq.process(client);
}
protected UpdateResponse del(SolrClient client, SolrParams params, Object... ids) throws IOException, SolrServerException {
UpdateRequest ureq = new UpdateRequest();
ureq.setParams(new ModifiableSolrParams(params));
for (Object id: ids) {
ureq.deleteById(id.toString());
}
return ureq.process(client);
}
protected UpdateResponse delQ(SolrClient client, SolrParams params, String... queries) throws IOException, SolrServerException {
UpdateRequest ureq = new UpdateRequest();
ureq.setParams(new ModifiableSolrParams(params));
for (String q: queries) {
ureq.deleteByQuery(q);
}
return ureq.process(client);
}
protected void index_specific(int serverNumber, Object... fields) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
for (int i = 0; i < fields.length; i += 2) {
doc.addField((String) (fields[i]), fields[i + 1]);
}
controlClient.add(doc);
SolrClient client = clients.get(serverNumber);
client.add(doc);
}
protected void del(String q) throws Exception {
controlClient.deleteByQuery(q);
for (SolrClient client : clients) {
client.deleteByQuery(q);
}
}// serial commit...
protected void commit() throws Exception {
controlClient.commit();
for (SolrClient client : clients) {
client.commit();
}
}
protected QueryResponse queryServer(ModifiableSolrParams params) throws SolrServerException, IOException {
// query a random server
int which = r.nextInt(clients.size());
SolrClient client = clients.get(which);
QueryResponse rsp = client.query(params);
return rsp;
}
/**
* Sets distributed params.
* Returns the QueryResponse from {@link #queryServer},
*/
protected QueryResponse query(Object... q) throws Exception {
return query(true, q);
}
/**
* Sets distributed params.
* Returns the QueryResponse from {@link #queryServer},
*/
protected QueryResponse query(SolrParams params) throws Exception {
return query(true, params);
}
/**
* Returns the QueryResponse from {@link #queryServer}
*/
protected QueryResponse query(boolean setDistribParams, Object[] q) throws Exception {
final ModifiableSolrParams params = createParams(q);
return query(setDistribParams, params);
}
/**
* Returns the QueryResponse from {@link #queryServer}
*/
protected QueryResponse query(boolean setDistribParams, SolrParams p) throws Exception {
final ModifiableSolrParams params = new ModifiableSolrParams(p);
// TODO: look into why passing true causes fails
params.set("distrib", "false");
final QueryResponse controlRsp = controlClient.query(params);
validateControlData(controlRsp);
if (shardCount == 0) {//mostly for temp debugging
return controlRsp;
}
params.remove("distrib");
if (setDistribParams) setDistributedParams(params);
QueryResponse rsp = queryServer(params);
compareResponses(rsp, controlRsp);
if (stress > 0) {
log.info("starting stress...");
Thread[] threads = new Thread[nThreads];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread() {
@Override
public void run() {
for (int j = 0; j < stress; j++) {
int which = r.nextInt(clients.size());
SolrClient client = clients.get(which);
try {
QueryResponse rsp = client.query(new ModifiableSolrParams(params));
if (verifyStress) {
compareResponses(rsp, controlRsp);
}
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
}
};
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
}
return rsp;
}
public QueryResponse queryAndCompare(SolrParams params, SolrClient... clients) throws SolrServerException, IOException {
return queryAndCompare(params, Arrays.<SolrClient>asList(clients));
}
public QueryResponse queryAndCompare(SolrParams params, Iterable<SolrClient> clients) throws SolrServerException, IOException {
QueryResponse first = null;
for (SolrClient client : clients) {
QueryResponse rsp = client.query(new ModifiableSolrParams(params));
if (first == null) {
first = rsp;
} else {
compareResponses(first, rsp);
}
}
return first;
}
public static boolean eq(String a, String b) {
return a == b || (a != null && a.equals(b));
}
public static int flags(Map<String, Integer> handle, Object key) {
if (key == null) return 0;
if (handle == null) return 0;
Integer f = handle.get(key);
return f == null ? 0 : f;
}
@SuppressWarnings({"unchecked"})
public static String compare(@SuppressWarnings({"rawtypes"})NamedList a,
@SuppressWarnings({"rawtypes"})NamedList b, int flags, Map<String, Integer> handle) {
// System.out.println("resp a:" + a);
// System.out.println("resp b:" + b);
boolean ordered = (flags & UNORDERED) == 0;
if (!ordered) {
@SuppressWarnings({"rawtypes"})
Map mapA = new HashMap(a.size());
for (int i=0; i<a.size(); i++) {
Object prev = mapA.put(a.getName(i), a.getVal(i));
}
@SuppressWarnings({"rawtypes"})
Map mapB = new HashMap(b.size());
for (int i=0; i<b.size(); i++) {
Object prev = mapB.put(b.getName(i), b.getVal(i));
}
return compare(mapA, mapB, flags, handle);
}
int posa = 0, posb = 0;
int aSkipped = 0, bSkipped = 0;
for (; ;) {
if (posa >= a.size() && posb >= b.size()) {
break;
}
String namea = null, nameb = null;
Object vala = null, valb = null;
int flagsa = 0, flagsb = 0;
while (posa < a.size()) {
namea = a.getName(posa);
vala = a.getVal(posa);
posa++;
flagsa = flags(handle, namea);
if ((flagsa & SKIP) != 0) {
namea = null; vala = null;
aSkipped++;
continue;
}
break;
}
while (posb < b.size()) {
nameb = b.getName(posb);
valb = b.getVal(posb);
posb++;
flagsb = flags(handle, nameb);
if ((flagsb & SKIP) != 0) {
nameb = null; valb = null;
bSkipped++;
continue;
}
if (eq(namea, nameb)) {
break;
}
return "." + namea + "!=" + nameb + " (unordered or missing)";
// if unordered, continue until we find the right field.
}
// ok, namea and nameb should be equal here already.
if ((flagsa & SKIPVAL) != 0) continue; // keys matching is enough
String cmp = compare(vala, valb, flagsa, handle);
if (cmp != null) return "." + namea + cmp;
}
if (a.size() - aSkipped != b.size() - bSkipped) {
return ".size()==" + a.size() + "," + b.size() + " skipped=" + aSkipped + "," + bSkipped;
}
return null;
}
public static String compare1(@SuppressWarnings({"rawtypes"})Map a,
@SuppressWarnings({"rawtypes"})Map b,
int flags, Map<String, Integer> handle) {
String cmp;
for (Object keya : a.keySet()) {
Object vala = a.get(keya);
int flagsa = flags(handle, keya);
if ((flagsa & SKIP) != 0) continue;
if (!b.containsKey(keya)) {
return "[" + keya + "]==null";
}
if ((flagsa & SKIPVAL) != 0) continue;
Object valb = b.get(keya);
cmp = compare(vala, valb, flagsa, handle);
if (cmp != null) return "[" + keya + "]" + cmp;
}
return null;
}
public static String compare(@SuppressWarnings({"rawtypes"})Map a,
@SuppressWarnings({"rawtypes"})Map b,
int flags, Map<String, Integer> handle) {
String cmp;
cmp = compare1(a, b, flags, handle);
if (cmp != null) return cmp;
return compare1(b, a, flags, handle);
}
public static String compare(SolrDocument a, SolrDocument b, int flags, Map<String, Integer> handle) {
return compare(a.getFieldValuesMap(), b.getFieldValuesMap(), flags, handle);
}
public static String compare(SolrDocumentList a, SolrDocumentList b, int flags, Map<String, Integer> handle) {
boolean ordered = (flags & UNORDERED) == 0;
String cmp;
int f = flags(handle, "maxScore");
if (f == 0) {
cmp = compare(a.getMaxScore(), b.getMaxScore(), 0, handle);
if (cmp != null) return ".maxScore" + cmp;
} else if ((f & SKIP) == 0) { // so we skip val but otherwise both should be present
assert (f & SKIPVAL) != 0;
if (b.getMaxScore() != null) {
if (a.getMaxScore() == null) {
return ".maxScore missing";
}
}
}
cmp = compare(a.getNumFound(), b.getNumFound(), 0, handle);
if (cmp != null) return ".numFound" + cmp;
cmp = compare(a.getStart(), b.getStart(), 0, handle);
if (cmp != null) return ".start" + cmp;
cmp = compare(a.size(), b.size(), 0, handle);
if (cmp != null) return ".size()" + cmp;
// only for completely ordered results (ties might be in a different order)
if (ordered) {
for (int i = 0; i < a.size(); i++) {
cmp = compare(a.get(i), b.get(i), 0, handle);
if (cmp != null) return "[" + i + "]" + cmp;
}
return null;
}
// unordered case
for (int i = 0; i < a.size(); i++) {
SolrDocument doc = a.get(i);
Object key = doc.getFirstValue("id");
SolrDocument docb = null;
if (key == null) {
// no id field to correlate... must compare ordered
docb = b.get(i);
} else {
for (int j = 0; j < b.size(); j++) {
docb = b.get(j);
if (key.equals(docb.getFirstValue("id"))) break;
}
}
// if (docb == null) return "[id="+key+"]";
cmp = compare(doc, docb, 0, handle);
if (cmp != null) return "[id=" + key + "]" + cmp;
}
return null;
}
public static String compare(Object[] a, Object[] b, int flags, Map<String, Integer> handle) {
if (a.length != b.length) {
return ".length:" + a.length + "!=" + b.length;
}
for (int i = 0; i < a.length; i++) {
String cmp = compare(a[i], b[i], flags, handle);
if (cmp != null) return "[" + i + "]" + cmp;
}
return null;
}
public static String compare(Object a, Object b, int flags, Map<String, Integer> handle) {
if (a == b) return null;
if (a == null || b == null) return ":" + a + "!=" + b;
if (a instanceof NamedList && b instanceof NamedList) {
return compare((NamedList) a, (NamedList) b, flags, handle);
}
if (a instanceof SolrDocumentList && b instanceof SolrDocumentList) {
return compare((SolrDocumentList) a, (SolrDocumentList) b, flags, handle);
}
if (a instanceof SolrDocument && b instanceof SolrDocument) {
return compare((SolrDocument) a, (SolrDocument) b, flags, handle);
}
if (a instanceof Map && b instanceof Map) {
return compare((Map) a, (Map) b, flags, handle);
}
if (a instanceof Object[] && b instanceof Object[]) {
return compare((Object[]) a, (Object[]) b, flags, handle);
}
if (a instanceof byte[] && b instanceof byte[]) {
if (!Arrays.equals((byte[]) a, (byte[]) b)) {
return ":" + a + "!=" + b;
}
return null;
}
if (a instanceof List && b instanceof List) {
return compare(((List) a).toArray(), ((List) b).toArray(), flags, handle);
}
// equivalent integer numbers
if ((a instanceof Integer || a instanceof Long) && (b instanceof Integer || b instanceof Long)) {
if (((Number)a).longValue() == ((Number)b).longValue()) {
return null;
} else {
return ":" + a + "!=" + b;
}
}
if ((flags & FUZZY) != 0) {
if ((a instanceof Double && b instanceof Double)) {
double aaa = ((Double) a).doubleValue();
double bbb = ((Double) b).doubleValue();
if (aaa == bbb || ((Double) a).isNaN() && ((Double) b).isNaN()) {
return null;
}
if ((aaa == 0.0) || (bbb == 0.0)) {
return ":" + a + "!=" + b;
}
double diff = Math.abs(aaa - bbb);
// When stats computations are done on multiple shards, there may
// be small differences in the results. Allow a small difference
// between the result of the computations.
double ratio = Math.max(Math.abs(diff / aaa), Math.abs(diff / bbb));
if (ratio > DOUBLE_RATIO_LIMIT) {
return ":" + a + "!=" + b;
} else {
return null;// close enough.
}
}
}
if (!(a.equals(b))) {
return ":" + a + "!=" + b;
}
return null;
}
protected void compareSolrResponses(SolrResponse a, SolrResponse b) {
// SOLR-3345: Checking QTime value can be skipped as there is no guarantee that the numbers will match.
handle.put("QTime", SKIPVAL);
// rf will be different since the control collection doesn't usually have multiple replicas
handle.put("rf", SKIPVAL);
String cmp = compare(a.getResponse(), b.getResponse(), flags, handle);
if (cmp != null) {
log.error("Mismatched responses:\n{}\n{}", a, b);
Assert.fail(cmp);
}
}
protected void compareResponses(QueryResponse a, QueryResponse b) {
if (System.getProperty("remove.version.field") != null) {
// we don't care if one has a version and the other doesnt -
// control vs distrib
// TODO: this should prob be done by adding an ignore on _version_ rather than mutating the responses?
if (a.getResults() != null) {
for (SolrDocument doc : a.getResults()) {
doc.removeFields("_version_");
}
}
if (b.getResults() != null) {
for (SolrDocument doc : b.getResults()) {
doc.removeFields("_version_");
}
}
}
{ // we don't care if one has a warnings section in the header and the other doesn't - control vs distrib
if (a.getHeader() != null) {
a.getHeader().remove("warnings");
}
if (b.getHeader() != null) {
b.getHeader().remove("warnings");
}
}
compareSolrResponses(a, b);
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ShardsRepeat {
public abstract int min() default 1;
public abstract int max() default DEFAULT_MAX_SHARD_COUNT;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ShardsFixed {
public abstract int num();
}
public class ShardsRepeatRule implements TestRule {
private abstract class ShardsStatement extends Statement {
abstract protected void callStatement() throws Throwable;
@Override
public void evaluate() throws Throwable {
distribSetUp();
if (! distribSetUpCalled) {
Assert.fail("One of the overrides of distribSetUp does not propagate the call.");
}
try {
callStatement();
} finally {
distribTearDown();
if (! distribTearDownCalled) {
Assert.fail("One of the overrides of distribTearDown does not propagate the call.");
}
}
}
}
private class ShardsFixedStatement extends ShardsStatement {
private final int numShards;
private final Statement statement;
private ShardsFixedStatement(int numShards, Statement statement) {
this.numShards = numShards;
this.statement = statement;
}
@Override
@SuppressWarnings({"rawtypes"})
public void callStatement() throws Throwable {
RandVal.uniqueValues = new HashSet(); // reset random values
fixShardCount(numShards);
try {
createServers(numShards);
statement.evaluate();
} finally {
destroyServers();
}
}
}
private class ShardsRepeatStatement extends ShardsStatement {
private final int min;
private final int max;
private final Statement statement;
private ShardsRepeatStatement(int min, int max, Statement statement) {
this.min = min;
this.max = max;
this.statement = statement;
}
@Override
@SuppressWarnings({"rawtypes"})
public void callStatement() throws Throwable {
for (shardCount = min; shardCount <= max; shardCount++) {
RandVal.uniqueValues = new HashSet(); //reset random values
createServers(shardCount);
try {
statement.evaluate();
} finally {
destroyServers();
}
}
}
}
@Override
public Statement apply(Statement statement, Description description) {
ShardsFixed fixed = description.getAnnotation(ShardsFixed.class);
ShardsRepeat repeat = description.getAnnotation(ShardsRepeat.class);
if (fixed != null && repeat != null) {
throw new RuntimeException("ShardsFixed and ShardsRepeat annotations can't coexist");
}
else if (fixed != null) {
return new ShardsFixedStatement(fixed.num(), statement);
}
else if (repeat != null) {
return new ShardsRepeatStatement(repeat.min(), repeat.max(), statement);
}
else {
return (isShardCountFixed ? new ShardsFixedStatement(shardCount, statement) :
new ShardsRepeatStatement(1, DEFAULT_MAX_SHARD_COUNT, statement));
}
}
}
@Rule
public ShardsRepeatRule repeatRule = new ShardsRepeatRule();
public static Object[] getRandFields(String[] fields, RandVal[] randVals) {
Object[] o = new Object[fields.length * 2];
for (int i = 0; i < fields.length; i++) {
o[i * 2] = fields[i];
o[i * 2 + 1] = randVals[i].uval();
}
return o;
}
/**
* Implementations can pre-test the control data for basic correctness before using it
* as a check for the shard data. This is useful, for instance, if a test bug is introduced
* causing a spelling index not to get built: both control &amp; shard data would have no results
* but because they match the test would pass. This method gives us a chance to ensure something
* exists in the control data.
*/
public void validateControlData(QueryResponse control) throws Exception {
/* no-op */
}
@SuppressWarnings({"unchecked"})
public static abstract class RandVal {
@SuppressWarnings({"rawtypes"})
public static Set uniqueValues = new HashSet();
public abstract Object val();
public Object uval() {
for (; ;) {
Object v = val();
if (uniqueValues.add(v)) return v;
}
}
}
public static class RandDate extends RandVal {
@Override
public Object val() {
long v = r.nextLong();
Date d = new Date(v);
return d.toInstant().toString();
}
}
protected String getSolrXml() {
return null;
}
/**
* Given a directory that will be used as the SOLR_HOME for a jetty instance, seeds that
* directory with the contents of {@link #getSolrHome} and ensures that the proper {@link #getSolrXml}
* file is in place.
*/
protected void seedSolrHome(File jettyHome) throws IOException {
FileUtils.copyDirectory(new File(getSolrHome()), jettyHome);
String solrxml = getSolrXml();
if (solrxml != null) {
FileUtils.copyFile(new File(getSolrHome(), solrxml), new File(jettyHome, "solr.xml"));
}
}
/**
* Given a directory that will be used as the <code>coreRootDirectory</code> for a jetty instance,
* Creates a core directory named {@link #DEFAULT_TEST_CORENAME} using a trivial
* <code>core.properties</code> if this file does not already exist.
*
* @see #writeCoreProperties(Path,String)
* @see #CORE_PROPERTIES_FILENAME
*/
private void seedCoreRootDirWithDefaultTestCore(Path coreRootDirectory) throws IOException {
// Kludgy and brittle with assumptions about writeCoreProperties, but i don't want to
// try to change the semantics of that method to ignore existing files
Path coreDir = coreRootDirectory.resolve(DEFAULT_TEST_CORENAME);
if (Files.notExists(coreDir.resolve(CORE_PROPERTIES_FILENAME))) {
writeCoreProperties(coreDir, DEFAULT_TEST_CORENAME);
} // else nothing to do, DEFAULT_TEST_CORENAME already exists
}
protected void setupJettySolrHome(File jettyHome) throws IOException {
seedSolrHome(jettyHome);
Files.createDirectories(jettyHome.toPath().resolve("cores").resolve("collection1"));
}
protected ModifiableSolrParams createParams(Object ... q) {
final ModifiableSolrParams params = new ModifiableSolrParams();
for (int i = 0; i < q.length; i += 2) {
params.add(q[i].toString(), q[i + 1].toString());
}
return params;
}
}