package lux.junit; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.transform.stream.StreamSource; import lux.Compiler; import lux.Evaluator; import lux.QueryContext; import lux.XdmResultSet; import lux.exception.LuxException; import lux.index.XmlIndexer; import lux.index.field.FieldDefinition.Type; import lux.index.field.XPathField; import lux.query.QNameQueryTest; import net.sf.saxon.s9api.DocumentBuilder; import net.sf.saxon.s9api.QName; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XdmItem; import net.sf.saxon.s9api.XdmNode; import net.sf.saxon.s9api.XdmValue; import org.apache.lucene.document.Field; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runners.ParentRunner; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; /** * Loads a test-suite file from the classpath whose name is the same as * this class, with an ".xml" extension. Runs one test for each test-case * in the file. */ public class QueryTestRunner extends ParentRunner<QueryTestCase> { private static final QName ID_QNAME = new QName("id"); private static final QName TYPE_QNAME = new QName("type"); // private String description; private HashMap<String, QueryTestCase> cases; private HashMap<String,XdmNode> queryMap = new HashMap<String, XdmNode>(); protected Evaluator eval; protected DocumentBuilder builder; public QueryTestRunner(Class<? extends QNameQueryTest> klass) throws InitializationError { super (klass); eval = new Evaluator(new Compiler(getIndexer().getConfiguration()), null, null); builder = eval.getCompiler().getProcessor().newDocumentBuilder(); queryMap = new HashMap<String, XdmNode>(); cases = new HashMap<String, QueryTestCase>(); try { eval.getCompiler().setSearchStrategy (Compiler.SearchStrategy.NONE); loadTests (); eval.getCompiler().setSearchStrategy (Compiler.SearchStrategy.LUX_SEARCH); } catch (IOException e) { throw new InitializationError(e); } catch (SaxonApiException e) { throw new InitializationError(e); } // update the compiler with the XPath fields eval.getCompiler().compileFieldExpressions(); } /** @return an indexer whose optimizations are to be tested. * @throws InitializationError */ private XmlIndexer getIndexer() throws InitializationError { QNameQueryTest test; try { test = (QNameQueryTest) getTestClass().getOnlyConstructor().newInstance(); } catch (Exception e) { throw new InitializationError (e); } return test.getIndexer(); } /** * @return a list of QueryTestCases that define the children of this Runner. */ @Override protected List<QueryTestCase> getChildren() { return new ArrayList<QueryTestCase>(cases.values()); } /** * Returns a {@link Description} for the {@code child} test case, an * element of the list returned by {@link ParentRunner#getChildren()} */ @Override protected Description describeChild(QueryTestCase child) { return Description.createTestDescription (getTestClass().getJavaClass(), child.getName()); } /** * Runs the test corresponding to {@code child}, which can be assumed to be * an element of the list returned by {@link ParentRunner#getChildren()}. * Subclasses are responsible for making sure that relevant test events are * reported through {@code notifier} */ @Override protected void runChild(final QueryTestCase child, RunNotifier notifier) { runLeaf (new Statement() { @Override public void evaluate () { child.evaluate(eval); } }, describeChild(child), notifier); } /** * Reads a file from the classpath whose name is the name of the test * class with an .xml extension, from the same package as the test class, * parses the document and creates the QueryTestCases from it. * @throws FileNotFoundException * @throws SaxonApiException * @throws InitializationError */ protected void loadTests () throws IOException, SaxonApiException, InitializationError { String suiteFileName = getTestClass().getJavaClass().getSimpleName() + ".xml"; XdmNode suite = readFile (suiteFileName); for (XdmItem queryItem : eval ("/test-suite/queries", suite)) { loadQueries (queryItem); } loadTestCases (suite); loadIndexes (suite); // description = evalStr ("/test-suite/meta/title", suite); // /test-suite/meta/setup/keys } private void loadTestCases(XdmNode top) { XdmValue kids = eval ("test-suite/test-cases/*", top); for (XdmItem item: kids) { XdmNode kid = (XdmNode) item; if (kid.getNodeName().getClarkName().equals("include")) { String fileName = kid.getAttributeValue(new QName("file")); XdmNode tests; try { tests = readFile (fileName); } catch (IOException e) { throw new LuxException ("Error reading file " + fileName, e); } catch (SaxonApiException e) { throw new LuxException ("Error reading file " + fileName, e); } loadTestCases (tests); } else if (kid.getNodeName().getClarkName().equals("test-case")) { addTestCase (kid); } } } private void loadIndexes(XdmNode top) throws IOException, SaxonApiException, InitializationError { XdmValue kids = eval ("/test-suite/setup/keys/key", top); for (XdmItem item: kids) { XdmNode key = (XdmNode) item; String keyID = key.getAttributeValue(ID_QNAME); String keyType = key.getAttributeValue(TYPE_QNAME); Type type = keyType == null ? Type.STRING : Type.valueOf(keyType.toUpperCase()); XPathField field = new XPathField(keyID, key.getStringValue(), null, Field.Store.YES, type); eval.getCompiler().getIndexConfiguration().addField (field); } } private void loadQueries(XdmItem top) { for (XdmItem queryItem : eval ("*", top)) { XdmNode node = (XdmNode) queryItem; if (node.getNodeName().getLocalName().equals("query")) { String queryID = evalStr ("@id", queryItem); XdmValue queries = eval ("*", queryItem); if (queries.size() > 0) { XdmNode firstQuery = (XdmNode) queries.iterator().next(); queryMap.put (queryID, firstQuery); } } else if (node.getNodeName().getLocalName().equals("include")) { String fileName = evalStr ("@file", node); XdmNode include; try { include = readFile (fileName); } catch (IOException e) { throw new LuxException("Failed to read file " + fileName, e); } catch (SaxonApiException e) { throw new LuxException("Failed to read file " + fileName, e); } XdmValue queries = eval("queries", include); for (XdmItem q : queries) { loadQueries (q); } } } } XdmNode readFile (String fileName) throws IOException, SaxonApiException { InputStream in = getTestClass().getJavaClass().getResourceAsStream (fileName); if (in == null) { throw new FileNotFoundException (fileName); } XdmNode node = builder.build(new StreamSource (in)); in.close(); return node; } private void addTestCase(XdmItem testItem) { String name = evalStr ("@name", testItem); String queryText = evalStr ("query", testItem); boolean expectError = evalStr ("exists(expect/error)", testItem).equals("true"); String expectedError = evalStr ("expect/error", testItem); List<XdmNode> expectedQueries = getExpectedQueries (testItem); String expectedResultType = evalStr ("expect/query[1]/@type", testItem); String expectedOrderBy= evalStr ("expect/query[1]/@order-by", testItem); QueryTestResult expectedResult = new QueryTestResult (expectError, expectedError, getExpectedQueryText(testItem), expectedQueries, expectedResultType, expectedOrderBy); QueryTestCase testCase = newTestCase (name, queryText, expectedResult); cases.put (name, testCase); } protected QueryTestCase newTestCase (String name, String queryText, QueryTestResult expectedResult) { return new QueryTestCase (name, queryText, expectedResult); } private String getExpectedQueryText (XdmItem testItem) { String expectedQueryText = evalStr ("expect/query", testItem); if (expectedQueryText != null) { // replace query id tokens like #QUERY# Pattern pat= Pattern.compile("#(\\w+)#"); Matcher matcher = pat.matcher(expectedQueryText); while (matcher.find()) { XdmNode query = queryMap.get(matcher.group(1)); if (query == null) { throw new RuntimeException ("Test case references undefined query id=" + matcher.group(1)); } expectedQueryText = matcher.replaceFirst(query.toString().replaceAll("\r?\n\\s*", "")); matcher = pat.matcher(expectedQueryText); } } return expectedQueryText; } private List<XdmNode> getExpectedQueries (XdmItem testItem) { List<XdmNode> expectedQueries = new ArrayList<XdmNode>(); for (XdmItem queryID : eval ("expect/query/@id", testItem)) { String expectedQueryID = queryID.getStringValue(); if (expectedQueryID != null) { XdmNode query = queryMap.get(expectedQueryID); if (query == null) { throw new RuntimeException ("Test case references undefined query id=" + expectedQueryID); } expectedQueries.add(query); } } return expectedQueries; } protected XdmValue eval (String xpath, XdmItem contextItem) { QueryContext context = new QueryContext(); context.setContextItem (contextItem); XdmResultSet result = eval.evaluate (xpath, context); return result.getXdmValue(); } protected String evalStr (String xpath, XdmItem contextItem) { // get the string value of the first item returned by evaluating the path, or empty string if none. XdmValue value = eval (xpath, contextItem); if (value.size() == 0) { return null; } XdmItem item = value.itemAt(0); return item.getStringValue(); } }