package net.contrapunctus.rngzip; import java.io.*; import java.util.Iterator; import java.util.LinkedList; import java.util.TreeSet; import net.contrapunctus.rngzip.io.RNGZInputStream; import net.contrapunctus.rngzip.io.RNGZOutputStream; import net.contrapunctus.rngzip.io.RNGZSettings; import net.contrapunctus.rngzip.util.BaliAutomaton; import net.contrapunctus.rngzip.util.ErrorReporter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.kohsuke.bali.automaton.TreeAutomaton; import org.kohsuke.bali.writer.*; import org.xml.sax.Attributes; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; import runtime.ValidateletImpl; /** * Test suite for a generic compress-decompress round-trip, using * files in tests/cases/ sub-directory. Not only do we look for * exceptions and assertion failures in the process, but we ensure * that the ultimate output is identical (in terms of SAX events) to * the original input. */ @RunWith(Parameterized.class) public class GenericTest { public static final String TEST_DIR = "tests/cases"; private XMLReader xmlReader; private ErrorReporter errorReporter; private ByteArrayOutputStream errorBytes; private PrintStream errorStream; private RNGZSettings settings; private String origFileName; // ---.---.xml private String schemaFileName; // ---.rng private byte[] compressedBytes; private EventRecorder origSax, newSax; @Before public void setup() throws Exception { errorBytes = new ByteArrayOutputStream(); errorStream = new PrintStream(errorBytes); errorReporter = new ErrorReporter(errorStream); xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setErrorHandler(errorReporter); settings = new RNGZSettings(); } @Parameterized.Parameters public static LinkedList<String[]> cases() { File testdir = new File(TEST_DIR); String[] tests = testdir.list(new FilenameFilter() { public boolean accept(File dir, String name) { return name.matches(".*\\..*\\.xml"); } }); LinkedList<String[]> args = new LinkedList<String[]>(); for(String t : tests) { args.add( new String[] { t } ); } return args; } public GenericTest(String name) { origFileName = TEST_DIR + File.separatorChar + name; schemaFileName = origFileName.replaceFirst("\\..*\\.xml", ".rng"); } @Test public void roundTrip() throws Exception { try { validateOnly(); recordOriginal(); compress(); decompress(); origSax.assertEqual(newSax); // If we get here, there were no exceptions. // But was there any output on the stream? errorStream.flush(); if( errorBytes.size() > 0 ) throw new Exception("error stream"); if( System.getProperty("DEBUG_TEST_CASES") != null ) { maybeSaveRNZ(); maybeSaveNewXML(); } } catch( Throwable th ) { maybeSaveRNZ(); maybeSaveNewXML(); String msg = composeErrorMessage(th.getMessage()); throw new Error( msg, th ); } } private void validateOnly() throws Exception { AutomatonWriter writer = new NullWriter(); Interpreter interpreter = new Interpreter(); writer = new MultiWriter(writer, interpreter); BaliAutomaton automaton = BaliAutomaton.fromRNG(new File(schemaFileName)); automaton.writeTo(writer); ValidateletImpl v = interpreter.createValidatelet(); xmlReader.setContentHandler(v); try { xmlReader.parse(origFileName); } catch( SAXParseException exn ) { errorStream.println("WARNING: Bali COULD NOT VALIDATE THIS TEST CASE"); } } private void recordOriginal() throws Exception { origSax = new EventRecorder(); xmlReader.setContentHandler(origSax); xmlReader.parse(origFileName); } private void compress() throws Exception { ByteArrayOutputStream bo = new ByteArrayOutputStream(); RNGZOutputStream ro = new RNGZOutputStream(bo, settings, null); GenericCompressor gc = new GenericCompressor (schemaFileName, errorReporter, ro); xmlReader.setContentHandler(gc); xmlReader.parse(origFileName); ro.close(); compressedBytes = bo.toByteArray(); } private void decompress() throws Exception { ByteArrayInputStream bi = new ByteArrayInputStream(compressedBytes); RNGZInputStream ri = new RNGZInputStream(bi, settings); newSax = new EventRecorder(); new GenericDecompressor(schemaFileName, ri, newSax); ri.close(); } private void maybeSaveRNZ() throws Exception { if( compressedBytes != null ) { String rnz = origFileName.replaceFirst("\\.xml$", ".rnz"); FileOutputStream fos = new FileOutputStream(rnz); fos.write(compressedBytes); fos.close(); errorStream.println("Saved " + rnz + " (" + compressedBytes.length + " bytes)"); } } private void maybeSaveNewXML() throws IOException { if( newSax != null ) { String xin = origFileName.replaceFirst("\\.xml$", ".xin"); String xout = origFileName.replaceFirst("\\.xml$", ".xout"); origSax.saveTo(xin); newSax.saveTo(xout); errorStream.println("Saved " + xin + " and " + xout); } } private String composeErrorMessage( String exnMesg ) { errorStream.close(); StringBuilder buf = new StringBuilder(); buf.append(exnMesg); buf.append("\nFailure in "); buf.append(origFileName); buf.append("\n"); if( errorBytes.size() > 0 ) { buf.append("Details follow:\n"); buf.append(new String(errorBytes.toByteArray())); } return buf.toString(); } /** * To compare the input against the output, we don't do it at the * character level; there are too many variations possible: spacing * and indentation, <foo/> vs. <foo></foo>, id="3" vs. id='3', etc. * So this ContentHandler will accumulate a history of events, for * comparison to another history. */ static class EventRecorder extends DefaultHandler { /* We store events as strings, the first character indicating the * kind of event, so that the document * <foo id='78' href='abc'>Yes!<bar/>No</foo> * becomes * * "+foo", "@href", "=abc", // attributes in alphabetical order * "@id", "=78", "$Yes!", * "+bar", "-bar", "$No", "-foo" * * Character events made entirely of white space are assumed to be * ignorable, and consecutive character events are merged. */ private LinkedList<String> history = new LinkedList<String>(); private TreeSet<String> keys = new TreeSet<String>(); public void startElement( String ns, String ln, String qn, Attributes at ) { history.add("+" + qn); /* We can't count on attributes being in alpha order, so add keys to a sorted set. */ keys.clear(); for( int i = 0; i < at.getLength(); i++ ) { keys.add( at.getQName(i) ); } for( String k : keys ) { history.add("@" + k); history.add("=" + at.getValue(k)); } } public void endElement( String ns, String ln, String qn ) { history.add("-" + qn); } public void characters( char[] ch, int start, int len ) { /* Check whether it's all white space. */ boolean ignoreable = true; for( int i = 0; i < len; i++ ) { if( ! Character.isWhitespace(ch[start+i]) ) { ignoreable = false; break; } } if( ignoreable ) return; String txt = new String(ch, start, len); /* Check whether last event was also a string. */ String last = history.removeLast(); if( last.charAt(0) == '$' ) { /* Merge consecutive strings. */ history.add(last + txt); } else { history.add(last); history.add('$' + txt); } } public int size() { return history.size(); } public void saveTo( String fileName ) throws IOException { PrintStream ps = new PrintStream(new FileOutputStream(fileName), false, "UTF-8"); for( String e : history ) { ps.println(e); } ps.close(); } /* Check whether two event histories are the same. */ public void assertEqual( EventRecorder that ) { Iterator<String> i = this.history.iterator(); Iterator<String> j = that.history.iterator(); while( i.hasNext() && j.hasNext() ) { String si = i.next(); String sj = j.next(); assert si.equals(sj) : "event mismatch: ["+si+"]["+sj+"]"; } assert i.hasNext() == j.hasNext() : "one event history shorter than other"; } } /* A little test program for the EventRecorder. */ public static void main(String[] args) throws Exception { XMLReader xr = XMLReaderFactory.createXMLReader(); EventRecorder er1 = new EventRecorder(); EventRecorder er2 = new EventRecorder(); xr.setContentHandler(er1); xr.parse(args[0]); xr.setContentHandler(er2); xr.parse(args[1]); er1.assertEqual(er2); } }