Home | History | Annotate | Download | only in xml
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package libcore.xml;
     18 
     19 import junit.framework.AssertionFailedError;
     20 import junit.framework.Test;
     21 import junit.framework.TestCase;
     22 import junit.framework.TestSuite;
     23 import org.w3c.dom.Element;
     24 import org.w3c.dom.Node;
     25 import org.w3c.dom.NodeList;
     26 import org.xml.sax.InputSource;
     27 import org.xml.sax.SAXException;
     28 
     29 import javax.xml.namespace.QName;
     30 import javax.xml.parsers.DocumentBuilderFactory;
     31 import javax.xml.parsers.ParserConfigurationException;
     32 import javax.xml.xpath.XPath;
     33 import javax.xml.xpath.XPathConstants;
     34 import javax.xml.xpath.XPathExpressionException;
     35 import javax.xml.xpath.XPathFactory;
     36 import javax.xml.xpath.XPathVariableResolver;
     37 import java.io.File;
     38 import java.io.IOException;
     39 import java.util.ArrayList;
     40 import java.util.List;
     41 
     42 /**
     43  * The implementation-independent part of the <a
     44  * href="http://jaxen.codehaus.org/">Jaxen</a> XPath test suite, adapted for use
     45  * by JUnit. To run these tests on a device:
     46  * <ul>
     47  *   <li>Obtain the Jaxen source from the project's website.
     48  *   <li>Copy the files to a device: <code>adb shell mkdir /data/jaxen ;
     49  *       adb push /home/dalvik-prebuild/jaxen /data/jaxen</code>
     50  *   <li>Invoke this class' main method, passing the on-device path to the test
     51  *       suite's root directory as an argument.
     52  * </ul>
     53  */
     54 public class JaxenXPathTestSuite {
     55 
     56     private static final String DEFAULT_JAXEN_HOME = "/home/dalvik-prebuild/jaxen";
     57 
     58     public static Test suite() throws Exception {
     59         String jaxenHome = System.getProperty("jaxen.home", DEFAULT_JAXEN_HOME);
     60         return suite(new File(jaxenHome));
     61     }
     62 
     63     /**
     64      * Creates a test suite from the Jaxen tests.xml catalog.
     65      */
     66     public static Test suite(File jaxenHome)
     67             throws ParserConfigurationException, IOException, SAXException {
     68 
     69         /*
     70          * The tests.xml document has this structure:
     71          *
     72          * <tests>
     73          *   <document url="...">
     74          *     <context .../>
     75          *     <context .../>
     76          *     <context .../>
     77          *   </document>
     78          *   <document url="...">
     79          *     <context .../>
     80          *   </document>
     81          * </tests>
     82          */
     83 
     84         File testsXml = new File(jaxenHome + "/xml/test/tests.xml");
     85         Element tests = DocumentBuilderFactory.newInstance()
     86                 .newDocumentBuilder().parse(testsXml).getDocumentElement();
     87 
     88         TestSuite result = new TestSuite();
     89         for (Element document : elementsOf(tests.getElementsByTagName("document"))) {
     90             String url = document.getAttribute("url");
     91             InputSource inputSource = new InputSource("file:" + jaxenHome + "/" + url);
     92             for (final Element context : elementsOf(document.getElementsByTagName("context"))) {
     93                 contextToTestSuite(result, url, inputSource, context);
     94             }
     95         }
     96 
     97         return result;
     98     }
     99 
    100     /**
    101      * Populates the test suite with tests from the given XML context element.
    102      */
    103     private static void contextToTestSuite(TestSuite suite, String url,
    104             InputSource inputSource, Element element) {
    105 
    106         /*
    107          * Each context element has this structure:
    108          *
    109          * <context select="...">
    110          *   <test .../>
    111          *   <test .../>
    112          *   <test .../>
    113          *   <valueOf .../>
    114          *   <valueOf .../>
    115          *   <valueOf .../>
    116          * </context>
    117          */
    118 
    119         String select = element.getAttribute("select");
    120         Context context = new Context(inputSource, url, select);
    121 
    122         XPath xpath = XPathFactory.newInstance().newXPath();
    123         xpath.setXPathVariableResolver(new ElementVariableResolver(element));
    124 
    125         for (Element test : elementsOf(element.getChildNodes())) {
    126             if (test.getTagName().equals("test")) {
    127                 suite.addTest(createFromTest(xpath, context, test));
    128 
    129             } else if (test.getTagName().equals("valueOf")) {
    130                 suite.addTest(createFromValueOf(xpath, context, test));
    131 
    132             } else {
    133                 throw new UnsupportedOperationException("Unsupported test: " + context);
    134             }
    135         }
    136     }
    137 
    138     /**
    139      * Returns the test described by the given {@code <test>} element. Such
    140      * tests come in one of three varieties:
    141      *
    142      * <ul>
    143      *   <li>Expected failures.
    144      *   <li>String matches. These tests have a nested {@code <valueOf>} element
    145      *       that sub-selects an expected text.
    146      *   <li>Count matches. These tests specify how many nodes are expected to
    147      *       match.
    148      * </ul>
    149      */
    150     private static TestCase createFromTest(
    151             final XPath xpath, final Context context, final Element element) {
    152         final String select = element.getAttribute("select");
    153 
    154         /* Such as <test exception="true" select="..." count="0"/> */
    155         if (element.getAttribute("exception").equals("true")) {
    156             return new XPathTest(context, select) {
    157                 @Override void test(Node contextNode) {
    158                     try {
    159                         xpath.evaluate(select, contextNode);
    160                         fail("Expected exception!");
    161                     } catch (XPathExpressionException expected) {
    162                     }
    163                 }
    164             };
    165         }
    166 
    167         /* a <test> with a nested <valueOf>, both of which have select attributes */
    168         NodeList valueOfElements = element.getElementsByTagName("valueOf");
    169         if (valueOfElements.getLength() == 1) {
    170             final Element valueOf = (Element) valueOfElements.item(0);
    171             final String valueOfSelect = valueOf.getAttribute("select");
    172 
    173             return new XPathTest(context, select) {
    174                 @Override void test(Node contextNode) throws XPathExpressionException {
    175                     Node newContext = (Node) xpath.evaluate(
    176                             select, contextNode, XPathConstants.NODE);
    177                     assertEquals(valueOf.getTextContent(),
    178                             xpath.evaluate(valueOfSelect, newContext, XPathConstants.STRING));
    179                 }
    180             };
    181         }
    182 
    183         /* Such as <test select="..." count="5"/> */
    184         final String count = element.getAttribute("count");
    185         if (count.length() > 0) {
    186             return new XPathTest(context, select) {
    187                 @Override void test(Node contextNode) throws XPathExpressionException {
    188                     NodeList result = (NodeList) xpath.evaluate(
    189                             select, contextNode, XPathConstants.NODESET);
    190                     assertEquals(Integer.parseInt(count), result.getLength());
    191                 }
    192             };
    193         }
    194 
    195         throw new UnsupportedOperationException("Unsupported test: " + context);
    196     }
    197 
    198     /**
    199      * Returns the test described by the given {@code <valueOf>} element. These
    200      * tests select an expected text.
    201      */
    202     private static TestCase createFromValueOf(
    203             final XPath xpath, final Context context, final Element element) {
    204         final String select = element.getAttribute("select");
    205         return new XPathTest(context, select) {
    206             @Override void test(Node contextNode) throws XPathExpressionException {
    207                 assertEquals(element.getTextContent(),
    208                         xpath.evaluate(select, contextNode, XPathConstants.STRING));
    209             }
    210         };
    211     }
    212 
    213     /**
    214      * The subject of an XPath query. This is itself defined by an XPath query,
    215      * so each test requires at least XPath expressions to be evaluated.
    216      */
    217     static class Context {
    218         private final InputSource inputSource;
    219         private final String url;
    220         private final String select;
    221 
    222         Context(InputSource inputSource, String url, String select) {
    223             this.inputSource = inputSource;
    224             this.url = url;
    225             this.select = select;
    226         }
    227 
    228         Node getNode() {
    229             XPath xpath = XPathFactory.newInstance().newXPath();
    230             try {
    231                 return (Node) xpath.evaluate(select, inputSource, XPathConstants.NODE);
    232             } catch (XPathExpressionException e) {
    233                 Error error = new AssertionFailedError("Failed to get context");
    234                 error.initCause(e);
    235                 throw error;
    236             }
    237         }
    238 
    239         @Override public String toString() {
    240             return url + " " + select;
    241         }
    242     }
    243 
    244     /**
    245      * This test evaluates an XPath expression against a context node and
    246      * compares the result to a known expectation.
    247      */
    248     public abstract static class XPathTest extends TestCase {
    249         private final Context context;
    250         private final String select;
    251 
    252         public XPathTest(Context context, String select) {
    253             super("test");
    254             this.context = context;
    255             this.select = select;
    256         }
    257 
    258         abstract void test(Node contextNode) throws XPathExpressionException;
    259 
    260         public final void test() throws XPathExpressionException {
    261             try {
    262                 test(context.getNode());
    263             } catch (XPathExpressionException e) {
    264                 if (isMissingFunction(e)) {
    265                     fail(e.getCause().getMessage());
    266                 } else {
    267                     throw e;
    268                 }
    269             }
    270         }
    271 
    272         private boolean isMissingFunction(XPathExpressionException e) {
    273             return e.getCause() != null
    274                     && e.getCause().getMessage().startsWith("Could not find function");
    275         }
    276 
    277         @Override public String getName() {
    278             return context + " " + select;
    279         }
    280     }
    281 
    282     /**
    283      * Performs XPath variable resolution by using {@code var:name="value"}
    284      * attributes from the given element.
    285      */
    286     private static class ElementVariableResolver implements XPathVariableResolver {
    287         private final Element element;
    288         public ElementVariableResolver(Element element) {
    289             this.element = element;
    290         }
    291         public Object resolveVariable(QName variableName) {
    292             return element.getAttribute("var:" + variableName.getLocalPart());
    293         }
    294     }
    295 
    296     private static List<Element> elementsOf(NodeList nodeList) {
    297         List<Element> result = new ArrayList<Element>();
    298         for (int i = 0; i < nodeList.getLength(); i++) {
    299             Node node = nodeList.item(i);
    300             if (node instanceof Element) {
    301                 result.add((Element) node);
    302             }
    303         }
    304         return result;
    305     }
    306 }
    307