/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2015 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package xquery;
import org.exist.Namespaces;
import org.exist.dom.memtree.SAXAdapter;
import org.exist.source.FileSource;
import org.exist.source.Source;
import org.exist.test.ExistXmldbEmbeddedServer;
import org.exist.util.FileUtils;
import org.exist.util.XMLFilenameFilter;
import org.exist.xmldb.XQueryService;
import org.exist.xquery.value.Sequence;
import org.junit.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.*;
import static org.junit.Assert.assertArrayEquals;
public abstract class TestRunner {
@ClassRule
public static final ExistXmldbEmbeddedServer existEmbeddedServer = new ExistXmldbEmbeddedServer(false, true);
protected abstract String getDirectory();
@Test
public void runXMLBasedTests() throws TransformerException, XMLDBException, ParserConfigurationException, SAXException, IOException {
final Path dir = Paths.get(getDirectory());
final List<Path> files;
if(Files.isDirectory(dir)) {
files = FileUtils.list(dir, XMLFilenameFilter.asPredicate());
} else if(new XMLFilenameFilter().accept(dir.getParent().toFile(), FileUtils.fileName(dir))) {
files = Arrays.asList(dir);
} else {
return;
}
final List<TestSuite> all = new ArrayList<>();
final XQueryService xqs = (XQueryService) existEmbeddedServer.getRoot().getService("XQueryService", "1.0");
final Source query = new FileSource(Paths.get("test/src/xquery/runTests.xql"), false);
if(files != null) {
for (final Path file : files) {
try {
final Document doc = parse(file);
xqs.declareVariable("doc", doc);
xqs.declareVariable("id", Sequence.EMPTY_SEQUENCE);
final ResourceSet result = xqs.execute(query);
final XMLResource resource = (XMLResource) result.getResource(0);
final List<TestSuite> tsResults = parseXmlResults((Element) resource.getContentAsDOM());
all.addAll(tsResults);
tsResults.forEach(this::printResults);
} catch (final Throwable t) {
System.err.println(t.getClass().getSimpleName() + " while running: " + file);
throw t;
}
}
}
assertSuccess(all);
}
@Test
public void runXQueryBasedTests() throws XMLDBException, IOException {
final Path dir = Paths.get(getDirectory());
final List<Path> suites = FileUtils.list(dir, path -> {
final String name = FileUtils.fileName(path);
return (!Files.isDirectory(path)) && Files.isReadable(path) && name.startsWith("suite") && name.endsWith(".xql");
});
final List<TestSuite> all = new ArrayList<>();
if(suites != null) {
for (final Path suite : suites) {
final XQueryService xqs = (XQueryService) existEmbeddedServer.getRoot().getService("XQueryService", "1.0");
xqs.setModuleLoadPath(getDirectory());
final Source query = new FileSource(suite, false);
final ResourceSet result = xqs.execute(query);
final XMLResource resource = (XMLResource) result.getResource(0);
final List<TestSuite> tsResults = parseXQueryResults((Element) resource.getContentAsDOM());
all.addAll(tsResults);
tsResults.forEach(this::printResults);
}
}
assertSuccess(all);
}
/**
* Uses JUnits assertArrayEquals to report test failures and errors
*/
private void assertSuccess(final List<TestSuite> tss) {
final List<String> expected = new ArrayList<>();
final List<String> actual = new ArrayList<>();
tss.forEach(ts ->
ts.getTestCases().forEach(tc -> {
if (tc instanceof TestCaseFailed) {
expected.add(((TestCaseFailed) tc).expected.orElse("{UNKNOWN EXPECTED: " + tc.name + "}"));
actual.add(((TestCaseFailed) tc).actual.orElse("{UNKNOWN ACTUAL: " + tc.name + "}"));
} else if(tc instanceof TestCaseError) {
expected.add("{UNKNOWN EXPECTED: " + tc.name + "}");
actual.add("{ERROR: " + ((TestCaseError)tc).reason + "}");
}
})
);
assertArrayEquals(expected.toArray(), actual.toArray());
}
/**
* Prints the results of a test suite to the console
*/
private void printResults(final TestSuite ts) {
System.out.println("XQuery Test suite: " + ts.getName());
ts.getTestCases().forEach(testCase -> {
System.out.println('\t' + testCase.toString());
});
}
/**
* Parses the output of eXist's XML based XQuery test suite
*
* @param testset The XML element <testset> from the test suite output
* @return The results of the tests
*/
private List<TestSuite> parseXmlResults(final Element testset) {
final List<TestSuite> results = new ArrayList<>();
final TestSuite ts = new TestSuite(getFirstChildElement(testset, "testName").map(this::getText).orElse("{UNKNOWN}"));
final NodeList nlTests = testset.getElementsByTagName("test");
for(int i = 0; i < nlTests.getLength(); i++) {
final Element test = (Element)nlTests.item(i);
final String name = test.getAttribute("n") + Optional.ofNullable(test.getAttribute("id")).map(id -> " (" + id + ")").orElse("");
final boolean pass = Boolean.parseBoolean(test.getAttribute("pass"));
final TestCase tc;
if(pass) {
tc = new TestCasePassed(name);
} else {
tc = getFirstChildElement(test, "result").map(result -> getFirstChildElement(result, "error").map(this::getText).<TestCase>map(err -> new TestCaseError(name, err))
.orElse(
new TestCaseFailed(name, getFirstChildElement(test, "task").map(this::getText).orElse("{UNKNOWN}"), getFirstChildElement(test, "expected").map(this::getText), Optional.of(getText(result)))
)
).orElse(null);
}
ts.add(tc);
}
results.add(ts);
return results;
}
/**
* Parses the output of eXist's XQuery based XQuery test suite
*
* @param testsuites The XML element <testsuites> from the test suite output
* @return The results of the tests
*/
private List<TestSuite> parseXQueryResults(final Element testsuites) {
final List<TestSuite> results = new ArrayList<>();
final NodeList nlTestSuite = testsuites.getElementsByTagName("testsuite");
for(int i = 0; i < nlTestSuite.getLength(); i++) {
final Element testsuite = (Element)nlTestSuite.item(i);
final TestSuite ts = new TestSuite(testsuite.getAttribute("package"));
final NodeList nlTestCase = testsuite.getElementsByTagName("testcase");
for(int j = 0; j < nlTestCase.getLength(); j++) {
final Element testcase = (Element)nlTestCase.item(j);
final Optional<Element> maybeFailure = getFirstChildElement(testcase, "failure");
final Optional<Element> maybeError = getFirstChildElement(testcase, "error");
final String name = testcase.getAttribute("name");
final TestCase tc = maybeFailure.<TestCase>map(failure -> {
final Optional<Element> output = getFirstChildElement(testcase, "output");
return new TestCaseFailed(name, failure.getAttribute("message"), Optional.of(getText(failure)), output.map(this::getText));
}).orElse(
maybeError.<TestCase>map(error ->
new TestCaseError(name, error.getAttribute("message"))
).orElse(
new TestCasePassed(name)
)
);
ts.add(tc);
}
results.add(ts);
}
return results;
}
/**
* Extracts all child text node values from an element
* (non-recursive)
*/
private final String getText(final Element elem) {
final StringBuilder builder = new StringBuilder();
final NodeList nlChildren = elem.getChildNodes();
for(int i = 0; i < nlChildren.getLength(); i++) {
final Node n = nlChildren.item(i);
if(n.getNodeType() == Node.TEXT_NODE) {
builder.append(n.getNodeValue());
}
}
return builder.toString();
}
/**
* Gets the first named child element from a parent element that matches
*/
private Optional<Element> getFirstChildElement(final Element parent, final String name) {
return Optional.of(parent.getElementsByTagName(name)).map(nl -> (Element)nl.item(0));
}
private class TestSuite {
private final String name;
private final List<TestCase> testCases = new ArrayList<>();
private TestSuite(final String name) {
this.name = name;
}
public final String getName() {
return name;
}
public void add(final TestCase testCase) {
testCases.add(testCase);
}
public List<TestCase> getTestCases() {
return testCases;
}
}
private abstract class TestCase {
protected final String name;
private TestCase(final String name) {
this.name = name;
}
@Override
public abstract String toString();
}
private class TestCasePassed extends TestCase {
private TestCasePassed(final String name) {
super(name);
}
@Override
public String toString() {
return "PASSED: " + name;
}
}
private class TestCaseError extends TestCase {
final String reason;
private TestCaseError(final String name, final String reason) {
super(name);
this.reason = reason;
}
@Override
public String toString() {
return "ERROR: " + name + ". " + reason + ".";
}
}
private class TestCaseFailed extends TestCase {
private final String reason;
private final Optional<String> expected;
private final Optional<String> actual;
private TestCaseFailed(final String name, final String reason, final Optional<String> expected, final Optional<String> actual) {
super(name);
this.reason = reason;
this.expected = expected;
this.actual = actual;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder()
.append("FAILED: ")
.append(name)
.append(". ")
.append(reason)
.append(".");
expected.map(e -> builder
.append(" Expected: '")
.append(e).append("'"));
actual.map(a -> builder
.append(" Actual: '")
.append(a).append("'"));
return builder.toString();
}
}
protected static Document parse(final Path file) throws IOException, SAXException, ParserConfigurationException {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
InputSource src = new InputSource(file.toUri().toASCIIString());
SAXParser parser = factory.newSAXParser();
XMLReader xr = parser.getXMLReader();
SAXAdapter adapter = new SAXAdapter();
xr.setContentHandler(adapter);
xr.setProperty(Namespaces.SAX_LEXICAL_HANDLER, adapter);
xr.parse(src);
return adapter.getDocument();
}
}