001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.scxml2.env.javascript;
019
020import java.io.StringReader;
021
022import javax.xml.xpath.XPath;
023import javax.xml.xpath.XPathConstants;
024import javax.xml.xpath.XPathFactory;
025
026import org.apache.commons.scxml2.Context;
027import org.apache.commons.scxml2.Evaluator;
028import org.apache.commons.scxml2.SCXMLExecutor;
029import org.apache.commons.scxml2.SCXMLExpressionException;
030import org.apache.commons.scxml2.SCXMLTestHelper;
031import org.apache.commons.scxml2.io.SCXMLReader;
032import org.junit.Assert;
033import org.junit.Before;
034import org.junit.Test;
035import org.w3c.dom.Element;
036import org.w3c.dom.Node;
037
038/** JUnit 3 test case for the JSEvaluator expression evaluator
039 *  class. Includes basic tests for:
040 *  <ul>
041 *  <li> constructor
042 *  <li> simple standard Javascript expressions
043 *  <li> Javascript expressions referencing SCXML &lt;var..&gt; variables.
044 *  <li> Javascript expressions referencing SCXML data model elements.
045 *  <li> Javascript expressions referencing SCXML data model locations.
046 *  <li> Javascript functions referencing SCXML context variables.
047 *  </ul>
048 */
049
050public class JSEvaluatorTest {
051    // TEST CONSTANTS
052
053    private static final String BAD_EXPRESSION = ">";
054    private static final String SCRIPT         = "<?xml version='1.0'?>" +
055                                                 "<scxml xmlns        = 'http://www.w3.org/2005/07/scxml' " +
056                                                        "xmlns:scxml  = 'http://commons.apache.org/scxml' " +
057                                                        "datamodel = 'ecmascript' " +
058                                                        "initial = 'start' "  +
059                                                        "version      = '1.0'>" +
060                                                  "<datamodel>"           +
061                                                  "<data id='forest'>"  +
062                                                   "<tree xmlns=''>"      +
063                                                   "<branch>"             +
064                                                   "<twig>leaf</twig>"    +
065                                                   "</branch>"            +
066                                                   "</tree>"              +
067                                                  "</data>"               +
068                                                  "</datamodel>"          +
069                                                  "<state id='start'>"              +
070                                                  "<transition target='end' />"     +
071                                                  "</state>"                        +
072                                                  "<state id='end' final='true' />" +
073                                                  "</scxml>";
074
075    private static final TestItem[] SIMPLE_EXPRESSIONS = {
076            new TestItem("'FIB: ' + (1 + 1 + 2 + 3 + 5)",new String("FIB: 12")),
077            new TestItem("1 + 1 + 2 + 3 + 5",            new Integer(12)), // Force comparison using intValue
078            new TestItem("1.1 + 1.1 + 2.1 + 3.1 + 5.1",  new Double(12.5)),
079            new TestItem("(1 + 1 + 2 + 3 + 5) == 12",    new Boolean(true)),
080            new TestItem("(1 + 1 + 2 + 3 + 5) == 13",    new Boolean(false)),
081    };
082
083    private static final TestItem[] VAR_EXPRESSIONS = {
084            new TestItem("'FIB: ' + fibonacci",new String("FIB: 12")),
085            new TestItem("fibonacci * 2",      new Double(24)),
086    };
087
088    private static final String FUNCTION = "function factorial(N) {\r\n" +
089                                                        "if (N == 1)\r\n"    +
090                                                        "   return N;\r\n"   +
091                                                        "\r\n"               +
092                                                        "return N * factorial(N-1);\r\n" +
093                                                "};\r\n" +
094                                                "\r\n"   +
095                                                "function fact5() {\r\n" +
096                                                "         return factorial(FIVE);\r\n" +
097                                                "};\r\n" +
098                                                "\r\n" +
099                                                "fact5()";
100
101    // TEST VARIABLES
102
103    private Context       context;
104    private Evaluator     evaluator;
105    private SCXMLExecutor fsm;
106
107    // TEST SETUP
108
109    /**
110     * Creates and initialises an SCXML data model in the context.
111     *
112     */
113    @Before
114    public void setUp() throws Exception {
115            fsm = SCXMLTestHelper.getExecutor(SCXMLReader.read(new StringReader(SCRIPT)));
116            evaluator = fsm.getEvaluator();
117            context = fsm.getGlobalContext();
118            context.set(Context.NAMESPACES_KEY,null);
119    }
120
121    // CLASS METHODS
122
123    /**
124     * Standalone test runtime.
125     *
126     */
127    public static void main(String args[]) {
128        String[] testCaseName = {JSEvaluatorTest.class.getName()};
129
130        junit.textui.TestRunner.main(testCaseName);
131    }
132
133    // INSTANCE METHOD TESTS
134
135    /**
136     * Ensures implementation of JSEvaluator default constructor and test basic
137     * expression evaluation.
138     *
139     */    
140    @Test
141    public void testBasic() throws SCXMLExpressionException {
142        Evaluator evaluator = new JSEvaluator();
143
144        Assert.assertNotNull(evaluator);
145        Assert.assertTrue   (((Boolean) evaluator.eval(context, "1+1 == 2")).booleanValue());
146    }
147
148    @Test
149    public void testScript() throws SCXMLExpressionException {
150        Evaluator evaluator = new JSEvaluator();
151        context.set("x", 3);
152        context.set("y", 0);
153        String script = 
154            "if ((x * 2.0) == 5.0) {" +
155                "y = 1.0;\n" +
156            "} else {\n" +
157                "y = 2.0;\n" +
158            "}";
159        Assert.assertEquals(2.0, evaluator.evalScript(context, script));
160        Assert.assertEquals(2.0, context.get("y"));
161    }
162
163    /**
164     * Tests handling of illegal expressions.
165     *
166     */    
167    @Test
168    public void testIllegalExpresssion() {
169        Evaluator evaluator = new JSEvaluator();
170
171        Assert.assertNotNull(evaluator);
172
173        try {
174            evaluator.eval(context,BAD_EXPRESSION);
175            Assert.fail          ("JSEvaluator should throw SCXMLExpressionException");
176
177        } catch (SCXMLExpressionException x) {
178            Assert.assertTrue("JSEvaluator: Incorrect error message",
179                       x.getMessage().startsWith("Error evaluating ['" + BAD_EXPRESSION + "']"));
180        }
181    }
182
183    /**
184     * Tests evaluation with simple standard expressions.
185     *
186     */    
187    @Test
188    public void testStandardExpressions() throws Exception {
189        for (TestItem item: SIMPLE_EXPRESSIONS) {
190            Object eval = evaluator.eval(context,item.expression);
191            if (item.result instanceof Integer && eval instanceof Number) {
192                Assert.assertEquals("Invalid result: " + item.expression,
193                        ((Integer) item.result).intValue(),
194                        ((Number) eval).intValue());
195            } else {
196                Assert.assertEquals("Invalid result: " + item.expression,
197                        item.result,
198                        eval);
199            }
200        }
201    }
202
203    /**
204     * Tests evaluation with SCXML context variables.
205     *
206     */    
207    @Test
208    public void testVarExpressions() throws Exception {
209        context.set("fibonacci",Integer.valueOf(12));
210
211        for (TestItem item: VAR_EXPRESSIONS) {
212            Assert.assertNotNull(context.get("fibonacci"));
213            Assert.assertEquals (Integer.valueOf(12),context.get("fibonacci"));
214            Assert.assertEquals ("Invalid result: " + item.expression,
215                          item.result,
216                          evaluator.eval(context,item.expression));
217        }
218    }
219
220    /**
221     * Tests evaluation with invalid SCXML context variables.
222     *
223     */    
224    @Test
225    public void testInvalidVarExpressions() {
226        for (TestItem item: VAR_EXPRESSIONS) {
227            try {
228                Assert.assertNull    (context.get("fibonacci"));
229                evaluator.eval(context,item.expression);
230                Assert.fail          ("Evaluated invalid <var... expression: " + item.expression);
231
232            } catch (SCXMLExpressionException x) {
233                // expected, ignore
234            }
235        }
236    }
237
238    /**
239     * Tests evaluation with SCXML data model expressions.
240     *
241     */    
242    @Test
243    public void testDataModelExpressions() throws Exception {
244        Assert.assertEquals("Invalid result: " + "Data('string($forest/tree/branch/twig)')",
245                     "leaf",
246                     evaluator.eval(context,"Data('string($forest/tree/branch/twig)')"));
247    }
248
249    /**
250     * Tests evaluation with invalid SCXML data model expressions.
251     *
252     */    
253    @Test
254    public void testInvalidDataModelExpressions() {
255        Assert.assertNull(context.get("forestx"));
256
257        try {
258            evaluator.eval(context,"Data(forestx,'string($forestx/tree/branch/twig)')");
259            Assert.fail          ("Evaluated invalid Data() expression: " + "Data('string($forestx/tree/branch/twig)')");
260
261        } catch (SCXMLExpressionException x) {
262            // expected, ignore
263        }
264    }
265
266    /**
267     * Tests evaluation of SCXML data model locations.
268     *
269     */    
270    @Test
271    public void testDataModelLocations() throws Exception {
272            Assert.assertNotNull(context.get("forest"));
273            XPath  xpath = XPathFactory.newInstance().newXPath();
274            Node   node  = (Node)   context.get("forest");
275            Node   twig  = (Node)   xpath.evaluate("tree/branch/twig", node, XPathConstants.NODE);
276
277            Assert.assertTrue  ("Invalid result: " + "Data(forest,'$forest/tree/branch/twig')",
278                          evaluator.eval(context,"Data('$forest/tree/branch/twig')") instanceof Element);
279
280            Assert.assertSame ("Incorrect node returned: " + "Data('$forest/tree/branch/twig')",
281                         twig,
282                         evaluator.eval(context,"Data('$forest/tree/branch/twig')"));
283    }
284
285    /**
286     * Tests evaluation of invalid SCXML data model locations.
287     *
288     */    
289    @Test
290    public void testInvalidDataModelLocations() throws Exception {
291            Assert.assertNotNull(context.get("forest"));
292            Assert.assertNull("Invalid result: " + "Data('$forest/tree/branch/twigx')",
293                       evaluator.eval(context,"Data('$forest/tree/branch/twigx')"));
294    }
295
296    /**
297     * Tests evaluation of Javascript functions with variables from SCXML context.
298     *
299     */    
300    @Test
301    public void testScriptFunctions() throws Exception {
302        context.set("FIVE",Integer.valueOf(5));
303        Assert.assertEquals(Integer.valueOf(5),context.get("FIVE"));
304        Assert.assertEquals("Invalid function result",Double.valueOf(120.0),evaluator.eval(context,FUNCTION));
305    }
306
307
308    // INNER CLASSES
309
310    private static class TestItem {
311        private String expression;
312        private Object result;
313
314        private TestItem(String expression,Object result) {
315            this.expression = expression;
316            this.result     = result;
317        }
318    }
319
320}
321