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