diff --git a/solr/core/src/test/org/apache/solr/spelling/suggest/TestPhraseSuggestions.java b/solr/core/src/test/org/apache/solr/spelling/suggest/TestPhraseSuggestions.java index f73f2f2407e..a556cda8936 100644 --- a/solr/core/src/test/org/apache/solr/spelling/suggest/TestPhraseSuggestions.java +++ b/solr/core/src/test/org/apache/solr/spelling/suggest/TestPhraseSuggestions.java @@ -17,16 +17,41 @@ package org.apache.solr.spelling.suggest; import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.SolrTestCaseJ4Test; +import org.apache.solr.SolrXPathTestCase; +import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.common.params.SpellingParams; +import org.apache.solr.util.EmbeddedSolrServerTestRule; import org.junit.BeforeClass; +import org.junit.ClassRule; -public class TestPhraseSuggestions extends SolrTestCaseJ4 { +public class TestPhraseSuggestions extends SolrXPathTestCase { static final String URI = "/suggest_wfst"; + @ClassRule + public static final EmbeddedSolrServerTestRule solrTestRule = new EmbeddedSolrServerTestRule(); + @BeforeClass public static void beforeClass() throws Exception { - initCore("solrconfig-phrasesuggest.xml", "schema-phrasesuggest.xml"); - assertQ(req("qt", URI, "q", "", SpellingParams.SPELLCHECK_BUILD, "true")); + // This was part of the SolrTestCaseJ4.setupTestCases method and appears to be needed. Ugh. + // Is this a direction we want, this randomness and need in SolrTestCase? + SolrTestCaseJ4Test.newRandomConfig(); + + solrTestRule.startSolr(SolrTestCaseJ4.TEST_HOME()); + solrTestRule + .newCollection() + .withConfigSet("../collection1") + .withConfigFile("conf/solrconfig-phrasesuggest.xml") + .withSchemaFile("conf/schema-phrasesuggest.xml") + .create(); + + assertQ( + solrTestRule.getSolrClient(), + req("qt", URI, "q", "", SpellingParams.SPELLCHECK_BUILD, "true")); + } + + public SolrClient getSolrClient() { + return solrTestRule.getSolrClient(); } public void test() { diff --git a/solr/test-framework/src/java/org/apache/solr/SolrClientTestHelpers.java b/solr/test-framework/src/java/org/apache/solr/SolrClientTestHelpers.java new file mode 100644 index 00000000000..b0871b57daa --- /dev/null +++ b/solr/test-framework/src/java/org/apache/solr/SolrClientTestHelpers.java @@ -0,0 +1,277 @@ +/* + * 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.StringWriter; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.client.solrj.request.QueryRequest; +import org.apache.solr.client.solrj.response.InputStreamResponseParser; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.client.solrj.response.XMLResponseParser; +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.util.BaseTestHarness; + +/** + * Helper methods for using assertQ/req style testing with SolrClient-based tests. This bridges the + * gap between TestHarness-style tests and EmbeddedSolrServerTestRule tests. + * + *

Example usage: + * + *

+ * import static org.apache.solr.SolrClientTestHelpers.assertQClient;
+ * import static org.apache.solr.SolrClientTestHelpers.params;
+ *
+ * SolrClient client = solrTestRule.getSolrClient();
+ * assertQClient(client, params("q", "*:*"),
+ *               "/response/lst[@name='responseHeader']/int[@name='status'][.='0']");
+ * 
+ */ +public class SolrClientTestHelpers { + + /** + * Creates a SolrParams object from key-value pairs, similar to req() in SolrTestCaseJ4. Use this + * with assertQClient(SolrClient, SolrParams, xpaths...). + * + * @param params alternating key-value pairs for query parameters + * @return SolrParams object + */ + public static SolrParams params(String... params) { + ModifiableSolrParams solrParams = new ModifiableSolrParams(); + for (int i = 0; i < params.length; i += 2) { + if (i + 1 < params.length) { + solrParams.add(params[i], params[i + 1]); + } + } + return solrParams; + } + + public static GenericSolrRequest req3(String... q) { + ModifiableSolrParams params = new ModifiableSolrParams(); + + if (q.length == 1) { + params.set("q", q); + } + + params.set("wt", "xml"); + + params.set("indent", params.get("indent", "off")); + + var req = + new GenericSolrRequest( + SolrRequest.METHOD.GET, "/select", params // .add("indent", "true") + ); + // Using the "smart" solr parsers won't work, because they decode into Solr objects. + // When trying to re-write into JSON, the JSONWriter doesn't have the right info to print it + // correctly. + // All we want to do is pass the JSON response to the user, so do that. + req.setResponseParser(new XMLResponseParser()); + + return req; + } + + public static QueryRequest req2(String... q) { + ModifiableSolrParams params = new ModifiableSolrParams(); + + if (q.length == 1) { + params.set("q", q); + } + if (q.length % 2 != 0) { + throw new RuntimeException( + "The length of the string array (query arguments) needs to be even"); + } + for (int i = 0; i < q.length; i += 2) { + params.set(q[i], q[i + 1]); + } + + params.set("wt", "xml"); + params.set("indent", params.get("indent", "off")); + + QueryRequest req = new QueryRequest(params); + String path = params.get("qt"); + if (path != null) { + req.setPath(path); + } + req.setResponseParser(new InputStreamResponseParser("xml")); + return req; + } + + public static ModifiableSolrParams params2(String... params) { + if (params.length % 2 != 0) throw new RuntimeException("Params length should be even"); + ModifiableSolrParams msp = new ModifiableSolrParams(); + for (int i = 0; i < params.length; i += 2) { + msp.add(params[i], params[i + 1]); + } + return msp; + } + + /** + * Executes a query using SolrClient with a custom message and validates against XPath + * expressions. + * + * @param message custom error message prefix + * @param client the SolrClient to execute the query against + * @param params the query parameters + * @param tests XPath expressions to validate against the response + */ + public static void assertQ2( + String message, SolrClient client, SolrParams params, String... tests) { + try { + // Ensure we request XML format for XPath validation + ModifiableSolrParams xmlParams = new ModifiableSolrParams(params); + if (xmlParams.get("wt") == null) { + xmlParams.set("wt", "xml"); + } + xmlParams.set("indent", xmlParams.get("indent", "off")); + + // Execute the query + QueryRequest req = new QueryRequest(xmlParams); + QueryResponse rsp = req.process(client); + + // Convert response to XML for XPath validation + String xml = toXML(rsp.getResponse()); + + // Validate using BaseTestHarness XPath validation + String results = BaseTestHarness.validateXPath(xml, tests); + + if (results != null) { + String msg = + (message == null ? "" : message + " ") + + "REQUEST FAILED: xpath=" + + results + + "\n\txml response was: " + + xml + + "\n\trequest was: " + + params; + throw new RuntimeException(msg); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Error executing query with SolrClient: " + params, e); + } + } + + /** + * Converts a NamedList response to XML string for XPath validation. This recreates the XML format + * that Solr produces. + * + * @param response the NamedList response to convert + * @return XML string representation + */ + private static String toXML(NamedList response) { + try { + StringWriter writer = new StringWriter(); + + // Write XML manually since we don't have a full XMLWriter context + writer.write("\n"); + writeNamedListAsXML(writer, response, "response", 0); + + return writer.toString(); + } catch (Exception e) { + throw new RuntimeException("Error converting response to XML", e); + } + } + + /** Recursively writes a NamedList as XML. */ + private static void writeNamedListAsXML( + StringWriter writer, NamedList nl, String rootName, int indent) { + String indentStr = " ".repeat(indent); + + if (rootName != null) { + writer.write(indentStr + "<" + rootName + ">\n"); + } + + for (int i = 0; i < nl.size(); i++) { + String name = nl.getName(i); + Object value = nl.getVal(i); + writeValueAsXML(writer, name, value, indent + 1); + } + + if (rootName != null) { + writer.write(indentStr + "\n"); + } + } + + /** Writes a value as XML. */ + private static void writeValueAsXML(StringWriter writer, String name, Object value, int indent) { + String indentStr = " ".repeat(indent); + + if (value == null) { + writer.write(indentStr + "\n"); + } else if (value instanceof NamedList) { + writer.write(indentStr + "\n"); + NamedList nl = (NamedList) value; + for (int i = 0; i < nl.size(); i++) { + writeValueAsXML(writer, nl.getName(i), nl.getVal(i), indent + 1); + } + writer.write(indentStr + "\n"); + } else if (value instanceof Iterable) { + writer.write(indentStr + "\n"); + for (Object item : (Iterable) value) { + writeValueAsXML(writer, null, item, indent + 1); + } + writer.write(indentStr + "\n"); + } else { + String type = getXMLType(value); + if (name != null) { + writer.write( + indentStr + + "<" + + type + + " name=\"" + + xmlEscape(name) + + "\">" + + xmlEscape(String.valueOf(value)) + + "\n"); + } else { + writer.write( + indentStr + "<" + type + ">" + xmlEscape(String.valueOf(value)) + "\n"); + } + } + } + + /** Determines XML type tag for a value. */ + private static String getXMLType(Object value) { + if (value instanceof Integer || value instanceof Long) { + return "int"; + } else if (value instanceof Float || value instanceof Double) { + return "float"; + } else if (value instanceof Boolean) { + return "bool"; + } else if (value instanceof java.util.Date) { + return "date"; + } else { + return "str"; + } + } + + /** Escapes XML special characters. */ + private static String xmlEscape(String str) { + if (str == null) return ""; + return str.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} diff --git a/solr/test-framework/src/java/org/apache/solr/SolrXPathTestCase.java b/solr/test-framework/src/java/org/apache/solr/SolrXPathTestCase.java new file mode 100644 index 00000000000..d1863bd4b25 --- /dev/null +++ b/solr/test-framework/src/java/org/apache/solr/SolrXPathTestCase.java @@ -0,0 +1,122 @@ +/* + * 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.InputStream; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import javax.xml.xpath.XPathExpressionException; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.request.QueryRequest; +import org.apache.solr.client.solrj.response.InputStreamResponseParser; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.util.BaseTestHarness; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SolrXPathTestCase extends SolrTestCase { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + /** + * Executes a query using SolrClient and validates the XML response against XPath expressions. + * This provides a similar interface to assertQ() in SolrTestCaseJ4 but works with SolrClient. + * + * @param client the SolrClient to use for the request + * @param req the query parameters + * @param tests XPath expressions to validate against the response + * @see SolrTestCaseJ4#assertQ(String, SolrQueryRequest, String...) + */ + public static void assertQ(SolrClient client, QueryRequest req, String... tests) { + try { + + // Process request and extract raw response + QueryResponse rsp = req.process(client); + NamedList rawResponse = rsp.getResponse(); + InputStream stream = (InputStream) rawResponse.get("stream"); + String response = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + + String results = BaseTestHarness.validateXPath(response, tests); + + if (results != null) { + String msg = + "REQUEST FAILED: xpath=" + + results + + "\n\txml response was: " + + response + + "\n\trequest was:" + + req.getQueryParams(); + + fail(msg); + } + } catch (XPathExpressionException e1) { + throw new RuntimeException("XPath is invalid", e1); + } catch (Throwable e3) { + log.error("REQUEST FAILED: {}", req.getParams(), e3); + throw new RuntimeException("Exception during query", e3); + } + } + + /** + * Instance method that delegates to the static assertQ using the instance's SolrClient. This + * provides a convenient way to call assertQ from instance test methods. + * + * @param req the query parameters + * @param tests XPath expressions to validate against the response + */ + public void assertQ(QueryRequest req, String... tests) { + assertQ(getSolrClient(), req, tests); + } + + /** + * Generates a QueryRequest + * + * @see SolrTestCaseJ4#req(String...) + */ + public static QueryRequest req(String... q) { + ModifiableSolrParams params = new ModifiableSolrParams(); + + if (q.length == 1) { + params.set("q", q); + } + if (q.length % 2 != 0) { + throw new RuntimeException( + "The length of the string array (query arguments) needs to be even"); + } + for (int i = 0; i < q.length; i += 2) { + params.set(q[i], q[i + 1]); + } + + params.set("wt", "xml"); + params.set("indent", params.get("indent", "off")); + + QueryRequest req = new QueryRequest(params); + String path = params.get("qt"); + if (path != null) { + req.setPath(path); + } + req.setResponseParser(new InputStreamResponseParser("xml")); + return req; + } + + public SolrClient getSolrClient() { + throw new RuntimeException("This method needs to be overridden"); + } +}