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 */ 017package org.apache.commons.scxml2; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileOutputStream; 022import java.io.ObjectInputStream; 023import java.io.ObjectOutputStream; 024import java.io.Reader; 025import java.io.StringReader; 026import java.net.URL; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.List; 030import java.util.Set; 031 032import javax.xml.parsers.DocumentBuilderFactory; 033 034import org.apache.commons.scxml2.env.SimpleDispatcher; 035import org.apache.commons.scxml2.env.Tracer; 036import org.apache.commons.scxml2.io.SCXMLReader; 037import org.apache.commons.scxml2.io.SCXMLReader.Configuration; 038import org.apache.commons.scxml2.model.CustomAction; 039import org.apache.commons.scxml2.model.EnterableState; 040import org.apache.commons.scxml2.model.SCXML; 041import org.apache.commons.scxml2.model.TransitionTarget; 042import org.junit.Assert; 043import org.w3c.dom.Document; 044import org.xml.sax.InputSource; 045/** 046 * Helper methods for running SCXML unit tests. 047 */ 048public class SCXMLTestHelper { 049 050 /** 051 * Serialized Commons SCXML object model temporary store. 052 * Assumes the default build artifacts are generated in the 053 * "target" directory (so it can be removed via a clean build). 054 */ 055 public static final String SERIALIZATION_DIR = "target/serialization"; 056 public static final String SERIALIZATION_FILE_PREFIX = SERIALIZATION_DIR + "/scxml"; 057 public static final String SERIALIZATION_FILE_SUFFIX = ".ser"; 058 059 // Generate a unique sequence number for the serialization files 060 private static int sequence=0; 061 062 private synchronized static String getSequenceNumber() { 063 return Integer.toString(++sequence); 064 } 065 066 public static URL getResource(String name) { 067 return SCXMLTestHelper.class.getClassLoader().getResource(name); 068 } 069 070 public static SCXML parse(final String scxmlResource) throws Exception { 071 Assert.assertNotNull(scxmlResource); 072 return parse(getResource(scxmlResource), null); 073 } 074 075 public static SCXML parse(final URL url) throws Exception { 076 return parse(url, null); 077 } 078 079 public static SCXML parse(final String scxmlResource, final List<CustomAction> customActions) throws Exception { 080 Assert.assertNotNull(scxmlResource); 081 return parse(getResource(scxmlResource), customActions); 082 } 083 084 public static SCXML parse(final URL url, final List<CustomAction> customActions) throws Exception { 085 Assert.assertNotNull(url); 086 Configuration configuration = new Configuration(null, null, customActions); 087 SCXML scxml = SCXMLReader.read(url, configuration); 088 Assert.assertNotNull(scxml); 089 SCXML roundtrip = testModelSerializability(scxml); 090 return roundtrip; 091 } 092 093 public static SCXML parse(final Reader scxmlReader, final List<CustomAction> customActions) throws Exception { 094 Assert.assertNotNull(scxmlReader); 095 Configuration configuration = new Configuration(null, null, customActions); 096 SCXML scxml = SCXMLReader.read(scxmlReader, configuration); 097 Assert.assertNotNull(scxml); 098 SCXML roundtrip = testModelSerializability(scxml); 099 return roundtrip; 100 } 101 102 public static SCXMLExecutor getExecutor(final URL url) throws Exception { 103 return getExecutor(parse(url), null); 104 } 105 106 public static SCXMLExecutor getExecutor(final String scxmlResource) throws Exception { 107 return getExecutor(parse(scxmlResource), null); 108 } 109 110 public static SCXMLExecutor getExecutor(final SCXML scxml) throws Exception { 111 return getExecutor(scxml, null); 112 } 113 114 public static SCXMLExecutor getExecutor(final URL url, final Evaluator evaluator) throws Exception { 115 return getExecutor(parse(url), evaluator); 116 } 117 118 public static SCXMLExecutor getExecutor(final SCXML scxml, final Evaluator evaluator) throws Exception { 119 return getExecutor(scxml, evaluator, new SimpleDispatcher()); 120 } 121 122 public static SCXMLExecutor getExecutor(final SCXML scxml, final Evaluator evaluator, final EventDispatcher eventDispatcher) throws Exception { 123 Tracer trc = new Tracer(); 124 SCXMLExecutor exec = new SCXMLExecutor(evaluator, eventDispatcher, trc); 125 exec.setStateMachine(scxml); 126 exec.addListener(scxml, trc); 127 return exec; 128 } 129 130 public static TransitionTarget lookupTransitionTarget(SCXMLExecutor exec, String id) { 131 return exec.getStateMachine().getTargets().get(id); 132 } 133 134 public static Context lookupContext(SCXMLExecutor exec, String id) { 135 TransitionTarget tt = lookupTransitionTarget(exec, id); 136 if (tt == null || !(tt instanceof EnterableState)) { 137 return null; 138 } 139 return exec.getSCInstance().lookupContext((EnterableState)tt); 140 } 141 142 public static void assertState(SCXMLExecutor exec, String expectedStateId) throws Exception { 143 Set<EnterableState> currentStates = exec.getStatus().getStates(); 144 Assert.assertEquals("Expected 1 simple (leaf) state with id '" 145 + expectedStateId + "' but found " + currentStates.size() + " states instead.", 146 1, currentStates.size()); 147 Assert.assertEquals(expectedStateId, currentStates.iterator(). 148 next().getId()); 149 } 150 151 public static Set<EnterableState> fireEvent(SCXMLExecutor exec, String name) throws Exception { 152 return fireEvent(exec, name, null); 153 } 154 155 public static Set<EnterableState> fireEvent(SCXMLExecutor exec, String name, Object payload) throws Exception { 156 TriggerEvent[] evts = {new TriggerEvent(name, TriggerEvent.SIGNAL_EVENT, payload)}; 157 exec.triggerEvents(evts); 158 return exec.getStatus().getStates(); 159 } 160 161 public static Set<EnterableState> fireEvent(SCXMLExecutor exec, TriggerEvent te) throws Exception { 162 exec.triggerEvent(te); 163 return exec.getStatus().getStates(); 164 } 165 166 public static Set<EnterableState> fireEvents(SCXMLExecutor exec, TriggerEvent[] evts) throws Exception { 167 exec.triggerEvents(evts); 168 return exec.getStatus().getStates(); 169 } 170 171 public static void assertPostTriggerState(SCXMLExecutor exec, 172 String triggerEventName, String expectedStateId) throws Exception { 173 assertPostTriggerState(exec, triggerEventName, null, expectedStateId); 174 } 175 176 public static void assertPostTriggerState(SCXMLExecutor exec, 177 String triggerEventName, Object payload, String expectedStateId) throws Exception { 178 assertPostTriggerState(exec, new TriggerEvent(triggerEventName, 179 TriggerEvent.SIGNAL_EVENT, payload), expectedStateId); 180 } 181 182 public static void assertPostTriggerStates(SCXMLExecutor exec, 183 String triggerEventName, String[] expectedStateIds) throws Exception { 184 assertPostTriggerStates(exec, triggerEventName, null, expectedStateIds); 185 } 186 187 public static void assertPostTriggerStates(SCXMLExecutor exec, 188 String triggerEventName, Object payload, String[] expectedStateIds) throws Exception { 189 assertPostTriggerStates(exec, new TriggerEvent(triggerEventName, 190 TriggerEvent.SIGNAL_EVENT, payload), expectedStateIds); 191 } 192 193 public static void assertPostTriggerState(SCXMLExecutor exec, 194 TriggerEvent triggerEvent, String expectedStateId) throws Exception { 195 Set<EnterableState> currentStates = fireEvent(exec, triggerEvent); 196 Assert.assertEquals("Expected 1 simple (leaf) state with id '" 197 + expectedStateId + "' on firing event " + triggerEvent 198 + " but found " + currentStates.size() + " states instead.", 199 1, currentStates.size()); 200 Assert.assertEquals(expectedStateId, currentStates.iterator(). 201 next().getId()); 202 } 203 204 public static void assertPostTriggerStates(SCXMLExecutor exec, 205 TriggerEvent triggerEvent, String[] expectedStateIds) throws Exception { 206 if (expectedStateIds == null || expectedStateIds.length == 0) { 207 Assert.fail("Must specify an array of one or more " 208 + "expected state IDs"); 209 } 210 Set<EnterableState> currentStates = fireEvent(exec, triggerEvent); 211 int n = expectedStateIds.length; 212 Assert.assertEquals("Expected " + n + " simple (leaf) state(s) " 213 + " on firing event " + triggerEvent + " but found " 214 + currentStates.size() + " states instead.", 215 n, currentStates.size()); 216 List<String> expectedStateIdList = new ArrayList<String>(Arrays.asList(expectedStateIds)); 217 for (TransitionTarget tt : currentStates) { 218 String stateId = tt.getId(); 219 if(!expectedStateIdList.remove(stateId)) { 220 Assert.fail("Expected state with id '" + stateId 221 + "' in current states on firing event " 222 + triggerEvent); 223 } 224 } 225 Assert.assertEquals("More states in current configuration than those" 226 + "specified in the expected state ids '" + expectedStateIds 227 + "'", 0, expectedStateIdList.size()); 228 } 229 230 public static SCXML testModelSerializability(final SCXML scxml) throws Exception { 231 File fileDir = new File(SERIALIZATION_DIR); 232 if (!fileDir.exists()) { 233 fileDir.mkdirs(); 234 } 235 String filename = SERIALIZATION_FILE_PREFIX 236 + getSequenceNumber() + SERIALIZATION_FILE_SUFFIX; 237 SCXML roundtrip = null; 238 ObjectOutputStream out = 239 new ObjectOutputStream(new FileOutputStream(filename)); 240 out.writeObject(scxml); 241 out.close(); 242 ObjectInputStream in = 243 new ObjectInputStream(new FileInputStream(filename)); 244 roundtrip = (SCXML) in.readObject(); 245 in.close(); 246 return roundtrip; 247 } 248 249 public static SCXMLExecutor testInstanceSerializability(final SCXMLExecutor exec) throws Exception { 250 File fileDir = new File(SERIALIZATION_DIR); 251 if (!fileDir.exists()) { 252 fileDir.mkdirs(); 253 } 254 String filename = SERIALIZATION_FILE_PREFIX 255 + getSequenceNumber() + SERIALIZATION_FILE_SUFFIX; 256 ObjectOutputStream out = 257 new ObjectOutputStream(new FileOutputStream(filename)); 258 out.writeObject(exec.detachInstance()); 259 out.close(); 260 ObjectInputStream in = 261 new ObjectInputStream(new FileInputStream(filename)); 262 exec.attachInstance((SCInstance) in.readObject()); 263 in.close(); 264 return exec; 265 } 266 267 /** 268 * Parses a String containing XML source into a {@link Document}. 269 * 270 * @param xml The XML source as a String. 271 * @return The parsed {@link Document}. 272 */ 273 public static Document stringToXMLDocument(final String xml) { 274 try { 275 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 276 dbf.setNamespaceAware(true); 277 return dbf.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); 278 } catch (Exception e) { 279 throw new RuntimeException("Exception parsing String to Node:\n" + xml); 280 } 281 } 282 283 /** 284 * Discourage instantiation. 285 */ 286 private SCXMLTestHelper() { 287 super(); 288 } 289}