/**
* 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 org.apache.lucene.util.LuceneTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.XML;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.util.TestHarness;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import javax.xml.xpath.XPathExpressionException;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* A junit4 Solr test harness that extends LuceneTestCaseJ4.
* Unlike AbstractSolrTestCase, a new core is not created for each test method.
*
*/
public abstract class SolrTestCaseJ4 extends LuceneTestCaseJ4 {
@BeforeClass
public static void beforeClassSolrTestCase() throws Exception {
ignoreException("ignore_exception");
}
@AfterClass
public static void afterClassSolrTestCase() throws Exception {
deleteCore();
resetExceptionIgnores();
}
@Override
public void setUp() throws Exception {
super.setUp();
log.info("###Starting " + getName()); // returns <unknown>???
}
@Override
public void tearDown() throws Exception {
log.info("###Ending " + getName());
super.tearDown();
}
/** Call initCore in @BeforeClass to instantiate a solr core in your test class.
* deleteCore will be called for you via SolrTestCaseJ4 @AfterClass */
public static void initCore(String config, String schema) throws Exception {
initCore(config, schema, null);
}
/** Call initCore in @BeforeClass to instantiate a solr core in your test class.
* deleteCore will be called for you via SolrTestCaseJ4 @AfterClass */
public static void initCore(String config, String schema, String solrHome) throws Exception {
configString = config;
schemaString = schema;
if (solrHome != null) {
System.setProperty("solr.solr.home", solrHome);
}
initCore();
}
/** Causes an exception matching the regex pattern to not be logged. */
public static void ignoreException(String pattern) {
if (SolrException.ignorePatterns == null)
SolrException.ignorePatterns = new HashSet<String>();
SolrException.ignorePatterns.add(pattern);
}
public static void resetExceptionIgnores() {
SolrException.ignorePatterns = null;
ignoreException("ignore_exception"); // always ignore "ignore_exception"
}
protected static String getClassName() {
StackTraceElement[] stack = new RuntimeException("WhoAmI").fillInStackTrace().getStackTrace();
for (int i = stack.length-1; i>=0; i--) {
StackTraceElement ste = stack[i];
String cname = ste.getClassName();
if (cname.indexOf(".lucene.")>=0 || cname.indexOf(".solr.")>=0) {
return cname;
}
}
return SolrTestCaseJ4.class.getName();
}
protected static String getSimpleClassName() {
String cname = getClassName();
return cname.substring(cname.lastIndexOf('.')+1);
}
protected static String configString;
protected static String schemaString;
protected static SolrConfig solrConfig;
/**
* Harness initialized by initTestHarness.
*
* <p>
* For use in test methods as needed.
* </p>
*/
protected static TestHarness h;
/**
* LocalRequestFactory initialized by initTestHarness using sensible
* defaults.
*
* <p>
* For use in test methods as needed.
* </p>
*/
protected static TestHarness.LocalRequestFactory lrf;
/**
* Subclasses must define this method to return the name of the
* schema.xml they wish to use.
*/
public static String getSchemaFile() {
return schemaString;
};
/**
* Subclasses must define this method to return the name of the
* solrconfig.xml they wish to use.
*/
public static String getSolrConfigFile() {
return configString;
};
/**
* The directory used to story the index managed by the TestHarness h
*/
protected static File dataDir;
/**
* Initializes things your test might need
*
* <ul>
* <li>Creates a dataDir in the "java.io.tmpdir"</li>
* <li>initializes the TestHarness h using this data directory, and getSchemaPath()</li>
* <li>initializes the LocalRequestFactory lrf using sensible defaults.</li>
* </ul>
*
*/
public static Logger log = LoggerFactory.getLogger(SolrTestCaseJ4.class);
private static String factoryProp;
public static void createTempDir() {
String cname = getSimpleClassName();
dataDir = new File(TEMP_DIR,
"solrtest-" + cname + "-" + System.currentTimeMillis());
dataDir.mkdirs();
}
public static void initCore() throws Exception {
log.info("####initCore");
ignoreException("ignore_exception");
factoryProp = System.getProperty("solr.directoryFactory");
if (factoryProp == null) {
System.setProperty("solr.directoryFactory","solr.RAMDirectoryFactory");
}
createTempDir();
// other methods like starting a jetty instance need these too
System.setProperty("solr.test.sys.prop1", "propone");
System.setProperty("solr.test.sys.prop2", "proptwo");
String configFile = getSolrConfigFile();
if (configFile != null) {
solrConfig = h.createConfig(getSolrConfigFile());
h = new TestHarness( dataDir.getAbsolutePath(),
solrConfig,
getSchemaFile());
lrf = h.getRequestFactory
("standard",0,20,"version","2.2");
}
log.info("####initCore end");
}
/** Subclasses that override setUp can optionally call this method
* to log the fact that their setUp process has ended.
*/
public void postSetUp() {
log.info("####POSTSETUP " + getName());
}
/** Subclasses that override tearDown can optionally call this method
* to log the fact that the tearDown process has started. This is necessary
* since subclasses will want to call super.tearDown() at the *end* of their
* tearDown method.
*/
public void preTearDown() {
log.info("####PRETEARDOWN " + getName());
}
/**
* Shuts down the test harness, and makes the best attempt possible
* to delete dataDir, unless the system property "solr.test.leavedatadir"
* is set.
*/
public static void deleteCore() throws Exception {
log.info("###deleteCore" );
if (h != null) { h.close(); }
if (dataDir != null) {
String skip = System.getProperty("solr.test.leavedatadir");
if (null != skip && 0 != skip.trim().length()) {
System.err.println("NOTE: per solr.test.leavedatadir, dataDir will not be removed: " + dataDir.getAbsolutePath());
} else {
if (!recurseDelete(dataDir)) {
System.err.println("!!!! WARNING: best effort to remove " + dataDir.getAbsolutePath() + " FAILED !!!!!");
}
}
}
if (factoryProp == null) {
System.clearProperty("solr.directoryFactory");
}
dataDir = null;
solrConfig = null;
h = null;
lrf = null;
configString = schemaString = null;
}
/** Validates an update XML String is successful
*/
public static void assertU(String update) {
assertU(null, update);
}
/** Validates an update XML String is successful
*/
public static void assertU(String message, String update) {
checkUpdateU(message, update, true);
}
/** Validates an update XML String failed
*/
public static void assertFailedU(String update) {
assertFailedU(null, update);
}
/** Validates an update XML String failed
*/
public static void assertFailedU(String message, String update) {
checkUpdateU(message, update, false);
}
/** Checks the success or failure of an update message
*/
private static void checkUpdateU(String message, String update, boolean shouldSucceed) {
try {
String m = (null == message) ? "" : message + " ";
if (shouldSucceed) {
String res = h.validateUpdate(update);
if (res != null) fail(m + "update was not successful: " + res);
} else {
String res = h.validateErrorUpdate(update);
if (res != null) fail(m + "update succeeded, but should have failed: " + res);
}
} catch (SAXException e) {
throw new RuntimeException("Invalid XML", e);
}
}
/** Validates a query matches some XPath test expressions and closes the query */
public static void assertQ(SolrQueryRequest req, String... tests) {
assertQ(null, req, tests);
}
/** Validates a query matches some XPath test expressions and closes the query */
public static void assertQ(String message, SolrQueryRequest req, String... tests) {
try {
String m = (null == message) ? "" : message + " ";
String response = h.query(req);
if (req.getParams().getBool("facet", false)) {
// add a test to ensure that faceting did not throw an exception
// internally, where it would be added to facet_counts/exception
String[] allTests = new String[tests.length+1];
System.arraycopy(tests,0,allTests,1,tests.length);
allTests[0] = "*[count(//lst[@name='facet_counts']/*[@name='exception'])=0]";
tests = allTests;
}
String results = h.validateXPath(response, tests);
if (null != results) {
fail(m + "query failed XPath: " + results +
"\n xml response was: " + response +
"\n request was: " + req.getParamString());
}
} catch (XPathExpressionException e1) {
throw new RuntimeException("XPath is invalid", e1);
} catch (Exception e2) {
throw new RuntimeException("Exception during query", e2);
}
}
/** Validates a query matches some JSON test expressions and closes the query.
* The text expression is of the form path:JSON. To facilitate easy embedding
* in Java strings, the JSON can have double quotes replaced with single quotes.
*
* Please use this with care: this makes it easy to match complete structures, but doing so
* can result in fragile tests if you are matching more than what you want to test.
*
**/
public static void assertJQ(SolrQueryRequest req, String... tests) throws Exception {
SolrParams params = null;
try {
params = req.getParams();
if (!"json".equals(params.get("wt","xml")) || params.get("indent")==null) {
ModifiableSolrParams newParams = new ModifiableSolrParams(params);
newParams.set("wt","json");
if (params.get("indent")==null) newParams.set("indent","true");
req.setParams(newParams);
}
String response;
boolean failed=true;
try {
response = h.query(req);
failed = false;
} finally {
if (failed) {
log.error("REQUEST FAILED: " + req.getParamString());
}
}
for (String test : tests) {
String testJSON = test.replace('\'', '"');
try {
failed = true;
String err = JSONTestUtil.match(response, testJSON);
failed = false;
if (err != null) {
log.error("query failed JSON validation. error=" + err +
"\n expected =" + testJSON +
"\n response = " + response +
"\n request = " + req.getParamString()
);
throw new RuntimeException(err);
}
} finally {
if (failed) {
log.error("JSON query validation threw an exception." +
"\n expected =" + testJSON +
"\n response = " + response +
"\n request = " + req.getParamString()
);
}
}
}
} finally {
// restore the params
if (params != null && params != req.getParams()) req.setParams(params);
}
}
/** Makes sure a query throws a SolrException with the listed response code */
public static void assertQEx(String message, SolrQueryRequest req, int code ) {
try {
h.query(req);
fail( message );
} catch (SolrException sex) {
assertEquals( code, sex.code() );
} catch (Exception e2) {
throw new RuntimeException("Exception during query", e2);
}
}
public static void assertQEx(String message, SolrQueryRequest req, SolrException.ErrorCode code ) {
try {
h.query(req);
fail( message );
} catch (SolrException e) {
assertEquals( code.code, e.code() );
} catch (Exception e2) {
throw new RuntimeException("Exception during query", e2);
}
}
/**
* @see TestHarness#optimize
*/
public static String optimize(String... args) {
return h.optimize(args);
}
/**
* @see TestHarness#commit
*/
public static String commit(String... args) {
return h.commit(args);
}
/**
* Generates a simple <add><doc>... XML String with no options
*
* @param fieldsAndValues 0th and Even numbered args are fields names odds are field values.
* @see #add
* @see #doc
*/
public static String adoc(String... fieldsAndValues) {
Doc d = doc(fieldsAndValues);
return add(d);
}
/**
* Generates a simple <add><doc>... XML String with no options
*/
public static String adoc(SolrInputDocument sdoc) {
List<String> fields = new ArrayList<String>();
for (SolrInputField sf : sdoc) {
for (Object o : sf.getValues()) {
fields.add(sf.getName());
fields.add(o.toString());
}
}
return adoc(fields.toArray(new String[fields.size()]));
}
/**
* Generates an <add><doc>... XML String with options
* on the add.
*
* @param doc the Document to add
* @param args 0th and Even numbered args are param names, Odds are param values.
* @see #add
* @see #doc
*/
public static String add(Doc doc, String... args) {
try {
StringWriter r = new StringWriter();
// this is anoying
if (null == args || 0 == args.length) {
r.write("<add>");
r.write(doc.xml);
r.write("</add>");
} else {
XML.writeUnescapedXML(r, "add", doc.xml, (Object[])args);
}
return r.getBuffer().toString();
} catch (IOException e) {
throw new RuntimeException
("this should never happen with a StringWriter", e);
}
}
/**
* Generates a <delete>... XML string for an ID
*
* @see TestHarness#deleteById
*/
public static String delI(String id) {
return h.deleteById(id);
}
/**
* Generates a <delete>... XML string for an query
*
* @see TestHarness#deleteByQuery
*/
public static String delQ(String q) {
return h.deleteByQuery(q);
}
/**
* Generates a simple <doc>... XML String with no options
*
* @param fieldsAndValues 0th and Even numbered args are fields names, Odds are field values.
* @see TestHarness#makeSimpleDoc
*/
public static Doc doc(String... fieldsAndValues) {
Doc d = new Doc();
d.xml = h.makeSimpleDoc(fieldsAndValues).toString();
return d;
}
/**
* Generates a SolrQueryRequest using the LocalRequestFactory
* @see #lrf
*/
public static SolrQueryRequest req(String... q) {
return lrf.makeRequest(q);
}
/**
* Generates a SolrQueryRequest using the LocalRequestFactory
* @see #lrf
*/
public static SolrQueryRequest req(String[] params, String... moreParams) {
String[] allParams = moreParams;
if (params.length!=0) {
int len = params.length + moreParams.length;
allParams = new String[len];
System.arraycopy(params,0,allParams,0,params.length);
System.arraycopy(moreParams,0,allParams,params.length,moreParams.length);
}
return lrf.makeRequest(allParams);
}
/** Neccessary to make method signatures un-ambiguous */
public static class Doc {
public String xml;
public String toString() { return xml; }
}
public static boolean recurseDelete(File f) {
if (f.isDirectory()) {
for (File sub : f.listFiles()) {
if (!recurseDelete(sub)) {
System.err.println("!!!! WARNING: best effort to remove " + sub.getAbsolutePath() + " FAILED !!!!!");
return false;
}
}
}
return f.delete();
}
public void clearIndex() {
assertU(delQ("*:*"));
}
}