/* * Redistribution and use of this software and associated documentation * ("Software"), with or without modification, are permitted provided * that the following conditions are met: * * 1. Redistributions of source code must retain copyright * statements and notices. Redistributions must also contain a * copy of this document. * * 2. Redistributions in binary form must reproduce the * above copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * 3. The name "Exolab" must not be used to endorse or promote * products derived from this Software without prior written * permission of Intalio, Inc. For written permission, * please contact info@exolab.org. * * 4. Products derived from this Software may not be called "Exolab" * nor may "Exolab" appear in their names without prior written * permission of Intalio, Inc. Exolab is a registered * trademark of Intalio, Inc. * * 5. Due credit should be given to the Exolab Project * (http://www.exolab.org/). * * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * * Copyright 2001-2003 (C) Intalio, Inc. All Rights Reserved. * * $Id: XMLTestCase.java 6787 2007-01-29 06:00:49Z ekuns $ */ package org.castor.xmlctf; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Method; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import junit.framework.TestCase; import org.castor.xmlctf.util.CTFUtils; import org.exolab.castor.mapping.Mapping; import org.exolab.castor.tests.framework.testDescriptor.CallMethod; import org.exolab.castor.tests.framework.testDescriptor.Configuration; import org.exolab.castor.tests.framework.testDescriptor.ConfigurationType; import org.exolab.castor.tests.framework.testDescriptor.FailureType; import org.exolab.castor.tests.framework.testDescriptor.ListenerType; import org.exolab.castor.tests.framework.testDescriptor.UnitTestCase; import org.exolab.castor.tests.framework.testDescriptor.Value; import org.exolab.castor.tests.framework.testDescriptor.types.FailureStepType; import org.exolab.castor.tests.framework.testDescriptor.types.TypeType; import org.exolab.castor.util.NestedIOException; import org.exolab.castor.xml.MarshalException; import org.exolab.castor.xml.MarshalListener; import org.exolab.castor.xml.Marshaller; import org.exolab.castor.xml.Unmarshaller; import org.exolab.castor.xml.XMLContext; import org.xml.sax.InputSource; /** * This class encapsulates all the common logic to run the test patterns for * Castor XML. Basically it handle the marshalling/marshalling/comparing. This * is used to factor the common code for the source generator test and the * mapping/introspection tests as only the setup differ in the test patterns. * <p> * This class is not complete and expects to be extended. * * @author <a href="mailto:gignoux@kernelcenter.org">Sebastien Gignoux</a> * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a> * @version $Revision: 6787 $ $Date: 2006-04-26 15:14:53 -0600 (Wed, 26 Apr 2006) $ */ public abstract class XMLTestCase extends TestCase { /** True if we desire a lot of info on what happen. */ public static boolean _verbose; /** True if we dump the stack trace for any exception that occurs during testing. */ protected static boolean _printStack; static { String v = System.getProperty(TestCaseAggregator.VERBOSE_PROPERTY); _verbose = (v!=null && v.equals(Boolean.TRUE.toString())); v = System.getProperty(TestCaseAggregator.PRINT_STACK_TRACE); _printStack = (v!=null && v.equals(Boolean.TRUE.toString())); } /** Name of the test suite to which this test belongs. */ protected String _suiteName; /** The name of the root class for this test. Must be set by a concrete class * if a test with a random object is used. */ protected String _rootClassName; /** The root class for this test. Must be set by a concrete class. */ protected Class _rootClass; /** If true, the dumpFields() function has been implemented in the root class. */ protected boolean _hasDump; /** The name of the mapping file to use in a Marshalling Framework Test Case. * Must be set by a concrete class if a test with a reference document is used. */ protected Mapping _mapping; /** A listener for marshalling. */ protected Object _listener; /** Type of listener test -- Marshal, Unmarshal or Both. */ protected TypeType _listenerType; /** Gold file for listener. */ protected String _listenerGoldFile; /** The Configuration the Marshalling Framework. */ protected Configuration _configuration; /** Name of this test. */ protected final String _name; /** The unit test case this class represent. */ protected final UnitTestCase _unitTest; /** Place where the temporary file have to be put. */ protected final File _outputRootFile; /** True if the test needs to be skipped. */ protected final boolean _skip; /** The failure object that is not null if the test is expected to fail. */ protected final FailureType _failure; /** Used only to retrieve the classloader. */ protected final CastorTestCase _test; /** The internal Castor XML context. */ private XMLContext _xmlContext; /** * Instantiate a new XMLTestCase with the given name. * * @param name the name of this test case. */ public XMLTestCase(final String name) { super(name); _name = cleanup(name); throw new IllegalArgumentException("You cannot use the name-only constructor"); } /** * Instantiates a new test case that represents the given UnitTestCase. * * @param test the reference to the jar/directory * @param unit the UnitTestCase that wraps the configuration for this XML Test case. */ public XMLTestCase(final CastorTestCase test, final UnitTestCase unit) { super(unit.getName()); _name = cleanup(unit.getName()); _unitTest = unit; _skip = unit.getSkip(); _failure = unit.getFailure(); _test = test; _outputRootFile = test.getOutputRootFile(); _xmlContext = null; } /** * Instantiates a new test cases that has the same configuration of the given * test case. * * @param name the name of the test case * @param tc the XML test case that hold the configuration for this test case */ public XMLTestCase(final String name, final XMLTestCase tc) { super(name); _name = cleanup(tc._name); _unitTest = tc._unitTest; _outputRootFile = tc._outputRootFile; _skip = tc._skip; _failure = tc._failure; _test = tc._test; _xmlContext = null; } protected abstract void setUp() throws Exception; protected abstract void tearDown() throws Exception; public void setXMLContext(XMLContext xmlContext) { _xmlContext = xmlContext; } public XMLContext getXMLContext() { return _xmlContext; } /** * Returns a version of the input name that is suitable for JUnit test case * or test suite use. Apparently, JUnit truncates test case names after * encountering certain characters, so we scrub those characters from the * test case or test suite name. * <p> * We also translate all whitespace to blanks, remove all leading and * trailing whitespace, and collapse consecutive whitespace to a single * blank. * * @param name * the input name * @return a name suitable for JUnit test case or test suite use. */ static String cleanup(final String name) { return name.replaceAll("[\\*()\\t\\n\\r\\f]", " ").replaceAll(" {2,}", " ").trim(); } /** * Sets the name of the test suite to which this test case belongs to. * * @param suiteName the name of the test suite. * */ public void setTestSuiteName(final String suiteName) { _suiteName = cleanup(suiteName); } /** * Returns the name of the test suite to which this test case belongs to. * @return the name of the test suite to which this test case belongs to. */ public String getTestSuiteName() { return _suiteName; } /** * Called when a test case throws an Exception. Returns true if we expected * this test to fail (and if this was the correct Exception class, if one is * specified, and if this was the correct test step for the failure to * occur, if one was specified). * * @param exception * the Exception that was thrown * @param checkStep * the test step that threw an Exception * @return true if we pass the test (because we expected this Exception) */ protected boolean checkExceptionWasExpected(final Exception exception, final FailureStepType checkStep) { if (_printStack) { exception.printStackTrace(); } // If we're not expecting any failure, then we're not expecting any Exception if (_failure == null || !_failure.getContent()) { return false; } if (checkStep == null) { fail("CTF coding failure ... checkStep should never be null"); } // We expect failure. Is this the failure we expected? String exceptionName = _failure.getException(); FailureStepType expectedStep = _failure.getFailureStep(); // If no specific step or Exception, then any failure is OK if (exceptionName == null && expectedStep == null) { return true; } // If we're expecting an exception, make sure we got the right one Exception ex = exception; if (ex instanceof NestedIOException) { ex = ((NestedIOException)ex).getException(); } if (exceptionName != null) { try { Class expected = Class.forName(exceptionName); if (!expected.isAssignableFrom(ex.getClass())) { String complaint = "Received Exception: '" + ex.getClass().getName() + ": " + ex + "' but expected: '" + exceptionName + "'."; StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); pw.print(complaint); if (_verbose) { pw.println("Stacktrace for the Exception that was thrown:"); ex.printStackTrace(pw); } pw.flush(); System.out.println(sw.toString()); fail(complaint); } } catch (ClassNotFoundException cnfex) { fail("The Exception specified: '" + exceptionName + "' cannot be found in the CLASSPATH"); } } // If we're expecting to fail at a specific step, make sure this is that step if (expectedStep != null && !expectedStep.equals(checkStep)) { fail("We expected to fail at test step '" + expectedStep.toString() + "' but actually failed at step '" + checkStep.toString() + "'"); } // We got the expected failure! Return true. return true; } /** * Marshals the object with the configuration of the test. * * @param object the object to marshall. * @param fileName the name of the file where to marshall the object. * @return a file containing marshalled output. */ protected File testMarshal(final Object object, final String fileName) { verbose("--> Marshaling to: '" + fileName + "'"); File marshalOutput = new File(_outputRootFile, fileName); try { Marshaller marshaller = createMarshaler(marshalOutput); marshaller.marshal(object); } catch (Exception e) { if (!checkExceptionWasExpected(e, FailureStepType.MARSHAL_TO_DISK)) { fail("Exception marshaling to disk " + e); } return null; } if (_failure != null && _failure.getContent() && _failure.getFailureStep() != null && _failure.getFailureStep().equals(FailureStepType.MARSHAL_TO_DISK)) { fail("Unmarshaling was expected to fail, but succeeded"); } return marshalOutput; } private Marshaller createMarshaler(final File marshalOutput) throws Exception { getXMLContext().getInternalContext().getXMLClassDescriptorResolver().cleanDescriptorCache(); Marshaller marshaller = getXMLContext().createMarshaller(); marshaller.setWriter(new FileWriter(marshalOutput)); //-- Configuration for marshaler? Use config from unit test case if available Configuration config = _unitTest.getConfiguration(); if (config == null) { config = _configuration; } if (config != null) { ConfigurationType marshal = config.getMarshal(); List returnValues = invokeEnumeratedMethods(marshaller, marshal); returnValues.clear(); // We don't care about the return values }//-- config != null if (_mapping != null) { marshaller.setMapping(_mapping); } if (_listener != null && _listener instanceof MarshalListener && _listenerType != TypeType.UNMARSHAL) { marshaller.setMarshalListener((MarshalListener)_listener); } return marshaller; } /** * Unmarshals the given file with the configuration of that test. If we * unmarshal null, complain and fail the test case. * * @param file the file containing the xml document to unmarshall. * @return the result of the unmarshalling of the given file. * @throws Exception if anything goes wrong during the test */ protected Object testUnmarshal(final File file) throws Exception { verbose("--> Unmarshaling '" + file.getName() + "'\n"); InputStream inputStream = new FileInputStream(file); Object unmarshaledObject = testUnmarshal(inputStream); inputStream.close(); assertNotNull("Unmarshaling '" + file.getName() + "' results in a NULL object.", unmarshaledObject); return unmarshaledObject; } /** * Unmarshals the given input stream with the configuration of that test. * * @param stream * the input stream containing the xml document to unmarshall. * @return the result of the unmarshalling of the given file, null if * anything went wrong. * @throws Exception if anything goes wrong during the test */ protected Object testUnmarshal(final InputStream stream) throws Exception { Object result = null; final Unmarshaller unmar = createUnmarshaler(); result = unmar.unmarshal(new InputSource(stream)); return result; } private Unmarshaller createUnmarshaler() throws Exception { //-- Configuration for unmarshaler? Use config from unit test case if available Configuration config = _unitTest.getConfiguration(); if (config == null) { config = _configuration; } final Unmarshaller unmar; if (_mapping != null) { //J unmar = new Unmarshaller(_mapping); unmar = getXMLContext().createUnmarshaller(); unmar.setMapping(_mapping); } else { if (_test.getClassLoader() != null) { //J unmar = new Unmarshaller(_rootClass, _test.getClassLoader()); unmar = getXMLContext().createUnmarshaller(); unmar.setClassLoader(_test.getClassLoader()); unmar.setClass(_rootClass); } else { //J unmar = new Unmarshaller(_rootClass); unmar = getXMLContext().createUnmarshaller(); unmar.setClass(_rootClass); } } if (_listener != null && _listener instanceof org.exolab.castor.xml.UnmarshalListener && _listenerType != TypeType.MARSHAL) { unmar.setUnmarshalListener((org.exolab.castor.xml.UnmarshalListener)_listener); } if (_listener != null && _listener instanceof org.castor.xml.UnmarshalListener && _listenerType != TypeType.MARSHAL) { unmar.setUnmarshalListener((org.castor.xml.UnmarshalListener)_listener); } unmar.setDebug(_verbose); if (config != null) { ConfigurationType unmarshal = config.getUnmarshal(); List returnValues = invokeEnumeratedMethods(unmar, unmarshal); returnValues.clear(); // We don't care about the return values } return unmar; } /** * Invokes all requested methods on the object we are supplied. Keeps track * of the objects returned from each invocation and returns a List containing * all of the return values. Some of the return values may be null. This * is not an error! * * @param objectInvoked the object on which to invoke the methods * @param config configuration object listing the methods we are to call * @throws java.lang.Exception if anything goes wrong during the test */ protected List invokeEnumeratedMethods(final Object objectInvoked, final ConfigurationType config) throws java.lang.Exception { final List returnValues = new LinkedList(); if (config == null) { return returnValues; } Enumeration methods = config.enumerateCallMethod(); //-- For each method defined, we invoke it on marshaller just created. while (methods.hasMoreElements()) { CallMethod method = (CallMethod) methods.nextElement(); //-- search for the method to invoke Method toBeinvoked = getInvokeMethod(objectInvoked.getClass(), method.getName(), method.getValue()); //-- construct the objects representing the arguments of the method Object[] arguments = getArguments(method.getValue()); //-- Invoke the method and keep a copy of the returned object returnValues.add(toBeinvoked.invoke(objectInvoked, arguments)); } return returnValues; } /** * Initialize listeners for marshalling/unmarshalling * * @param listener the listener to initialize * @throws ClassNotFoundException * @throws IllegalAccessException * @throws InstantiationException */ protected void initializeListeners(final ListenerType listener) throws ClassNotFoundException, IllegalAccessException, InstantiationException { String listenerClassName = listener.getClassName(); if (listenerClassName == null || listenerClassName.length() == 0) { throw new IllegalArgumentException("ClassName must be provided for Listener element."); } final Class listenerClass; if (_test.getClassLoader() != null) { listenerClass = _test.getClassLoader().loadClass(listenerClassName); } else { listenerClass = Thread.currentThread().getContextClassLoader().loadClass(listenerClassName); } Object o = listenerClass.newInstance(); if (o instanceof org.castor.xml.UnmarshalListener || o instanceof org.exolab.castor.xml.UnmarshalListener || o instanceof MarshalListener) { _listener = o; } else { _listener = null; } _listenerGoldFile = listener.getGoldFile(); _listenerType = listener.getType(); } /** * Returns an instance of the object model hardcoded in the given * ObjectModelBuilder. * * @param builderName the name of the class used as a builder * @return an instance of the object model hardcoded in the given * ObjectModelBuilder. * @throws java.lang.Exception if anything goes wrong during the test */ protected Object buildObjectModel(final String builderName) throws java.lang.Exception { Class builderClass = null; if (_test.getClassLoader() != null) { builderClass = _test.getClassLoader().loadClass(builderName); } else { builderClass = this.getClass().getClassLoader().loadClass(builderName); } ObjectModelBuilder builder = (ObjectModelBuilder)builderClass.newInstance(); return builder.buildInstance(); } private Method getInvokeMethod(final Class dataBinder, final String methodName, final Value[] values) throws ClassNotFoundException, NoSuchMethodException { if (methodName == null) { throw new IllegalArgumentException("The name of the method to invoke is null"); } Class[] argumentsClass = null; //-- the value object represents the arguments of the method if any if (values != null && values.length > 0) { argumentsClass = new Class[values.length]; for (int i = 0; i < values.length; i++) { Value value = values[i]; argumentsClass[i] = CTFUtils.getClass(value.getType(), _test.getClassLoader()); } } return dataBinder.getMethod(methodName, argumentsClass); } private Object[] getArguments(final Value[] values) throws ClassNotFoundException, MarshalException { Object[] result = new Object[values.length]; for (int i = 0; i < values.length; i++) { Value value = values[i]; result[i] = CTFUtils.instantiateObject(value.getType(), value.getContent(), _test.getClassLoader()); } return result; } /** * print the message if in verbose mode. * @param message the message to print */ protected void verbose(final String message) { if (_verbose) { System.out.println(message); } } }