package monty.solr.util;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.JSONTestUtil;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.common.params.CommonParams;
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.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.util.TestHarness;
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.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* A junit4 Solr test harness that extends LuceneTestCaseJ4. Unlike
* AbstractSolrTestCase, a new core is not created for each test method.
*
* This is a modified copy of org.apache.solr.SolrTestCaseJ4
*
*/
public abstract class MontySolrTestCaseJ4 extends LuceneTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
log.info("###Starting " + getTestName()); // returns <unknown>???
}
@Override
public void tearDown() throws Exception {
log.info("###Ending " + getTestName());
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, TEST_HOME);
}
/**
* 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 {
startTrackingSearchers();
configString = config;
schemaString = schema;
if (solrHome != null) {
System.setProperty("solr.solr.home", solrHome);
}
initCore();
}
static long numOpens;
static long numCloses;
protected static void startTrackingSearchers() {
numOpens = SolrIndexSearcher.numOpens.get();
numCloses = SolrIndexSearcher.numCloses.get();
}
protected static void endTrackingSearchers() {
long endNumOpens = SolrIndexSearcher.numOpens.get();
long endNumCloses = SolrIndexSearcher.numCloses.get();
if (endNumOpens - numOpens != endNumCloses - numCloses) {
String msg = "ERROR: SolrIndexSearcher opens="
+ (endNumOpens - numOpens) + " closes="
+ (endNumCloses - numCloses);
log.error(msg);
// fail(msg);
}
}
/** 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 MontySolrTestCaseJ4.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;
};
public static Path getSolrHome() {
return new File(System.getProperty("solr.solr.home")).toPath();
}
/**
* 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(MontySolrTestCaseJ4.class);
private static String factoryProp;
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");
}
if (dataDir == null) {
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) {
createCore();
}
log.info("####initCore end");
}
public static void createCore() {
solrConfig = TestHarness.createConfig(getSolrHome(), getSolrConfigFile());
h = new TestHarness( dataDir.getAbsolutePath(),
solrConfig,
getSchemaFile());
lrf = h.getRequestFactory
("standard",0,20,CommonParams.VERSION,"2.2");
}
/**
* 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 " + getTestName());
}
/**
* 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 " + getTestName());
}
/**
* 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;
endTrackingSearchers();
}
/**
* 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) {
String msg = "REQUEST FAILED: xpath=" + results
+ "\n\txml response was: " + response
+ "\n\trequest was:" + req.getParamString();
log.error(msg);
throw new RuntimeException(msg);
}
} catch (XPathExpressionException e1) {
throw new RuntimeException("XPath is invalid", e1);
} catch (Exception e2) {
SolrException.log(log, "REQUEST FAILED: " + req.getParamString(),
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;
}
public static ModifiableSolrParams params(String... params) {
ModifiableSolrParams msp = new ModifiableSolrParams();
for (int i = 0; i < params.length; i += 2) {
msp.add(params[i], params[i + 1]);
}
return msp;
}
/**
* 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);
}
/**
* Generates a SolrQueryRequest
*/
public static SolrQueryRequest req(SolrParams params, String... moreParams) {
ModifiableSolrParams mp = new ModifiableSolrParams(params);
for (int i = 0; i < moreParams.length; i += 2) {
mp.add(moreParams[i], moreParams[i + 1]);
}
return new LocalSolrQueryRequest(h.getCore(), mp);
}
/** Neccessary to make method signatures un-ambiguous */
public static class Doc {
public String xml;
@Override
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("*:*"));
}
/**
* Gets a resource from the context classloader as {@link File}. This method
* should only be used, if a real file is needed. To get a stream, code
* should prefer {@link Class#getResourceAsStream} using
* {@code this.getClass()}.
*/
public static File getFile(String name) {
try {
File file = new File(name);
if (!file.exists()) {
file = new File(Thread.currentThread().getContextClassLoader()
.getResource(name).toURI());
}
return file;
} catch (Exception e) {
/* more friendly than NPE */
throw new RuntimeException("Cannot find resource: " + name);
}
}
public static Throwable getRootCause(Throwable t) {
Throwable result = t;
for (Throwable cause = t; null != cause; cause = cause.getCause()) {
result = cause;
}
return result;
}
/**
* ================================================ modifications
* ================================================
*/
public static String MONTYSOLR_HOME = MontySolrSetup.getMontySolrHome();
private static final String SOLR_HOME = MontySolrSetup.getSolrHome();
private static final String SOURCE_HOME = MONTYSOLR_HOME
+ "/src/test-files";
public static String TEST_HOME = SOURCE_HOME + "/solr";
public static String WEBAPP_HOME = new File(SOLR_HOME, "src/webapp/web")
.getAbsolutePath();
public static String EXAMPLE_HOME = new File(SOLR_HOME, "example/solr")
.getAbsolutePath();
public static String EXAMPLE_MULTICORE_HOME = new File(SOLR_HOME,
"example/multicore").getAbsolutePath();
public static String EXAMPLE_SCHEMA = EXAMPLE_HOME + "/collection1/conf/schema.xml";
public static String EXAMPLE_CONFIG = EXAMPLE_HOME + "/collection1/conf/solrconfig.xml";
}