]> source.dussan.org Git - aspectj.git/commitdiff
initial version
authorwisberg <wisberg>
Mon, 16 Dec 2002 18:14:27 +0000 (18:14 +0000)
committerwisberg <wisberg>
Mon, 16 Dec 2002 18:14:27 +0000 (18:14 +0000)
124 files changed:
lib/commons/commons-src.zip [new file with mode: 0644]
lib/commons/commons.jar [new file with mode: 0644]
lib/readme-lib-module.html
testing-client/.classpath [new file with mode: 0644]
testing-client/.project [new file with mode: 0644]
testing-client/src/.cvsignore [new file with mode: 0644]
testing-client/src/org/aspectj/testing/Tester.java [new file with mode: 0644]
testing-client/testsrc/TestingClientModuleTests.java [new file with mode: 0644]
testing-util/.classpath [new file with mode: 0644]
testing-util/.project [new file with mode: 0644]
testing-util/TestUtilTest.launch [new file with mode: 0644]
testing-util/src/.cvsignore [new file with mode: 0644]
testing-util/src/org/aspectj/testing/util/TestUtil.java [new file with mode: 0644]
testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile$1.class [new file with mode: 0644]
testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile.class [new file with mode: 0644]
testing-util/testdata/testCompareTextFiles/differentFile/actual/TestUtilTest.java [new file with mode: 0644]
testing-util/testdata/testCompareTextFiles/differentFile/expected/TestUtilTest.java [new file with mode: 0644]
testing-util/testdata/testCompareTextFiles/sameFile/actual/TestUtilTest.java [new file with mode: 0644]
testing-util/testdata/testCompareTextFiles/sameFile/expected/TestUtilTest.java [new file with mode: 0644]
testing-util/testsrc/TestingUtilModuleTests.java [new file with mode: 0644]
testing-util/testsrc/org/aspectj/testing/util/TestCompareClassFile.java [new file with mode: 0644]
testing-util/testsrc/org/aspectj/testing/util/TestUtilTest.java [new file with mode: 0644]
testing-util/testsrc/org/aspectj/testing/util/UtilTests.java [new file with mode: 0644]
testing/.classpath [new file with mode: 0644]
testing/.project [new file with mode: 0644]
testing/src/.cvsignore [new file with mode: 0644]
testing/src/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java [new file with mode: 0644]
testing/src/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/gui.properties [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Back16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Delete24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Forward16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Open16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Open24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Quit24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Reload24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Save16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Save24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/SaveAs16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/SaveAs24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Start16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Start24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Stop16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Stop24.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Test16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Testfailed16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Testopen16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/gui/resources/images/Testpassed16.gif [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/AbstractRunSpec.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/AjcTest.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/CompilerRun.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/DirChanges.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/FlatSuiteReader.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/Globals.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/IAjcRun.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/IRunSpec.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/IncCompilerRun.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/JavaRun.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/RunSpecIterator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/Sandbox.java [new file with mode: 0644]
testing/src/org/aspectj/testing/harness/bridge/Validator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/IRun.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/IRunIterator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/IRunListener.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/IRunStatus.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/IRunValidator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/RunIterator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/RunListener.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/RunListeners.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/RunStatus.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/RunValidator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/Runner.java [new file with mode: 0644]
testing/src/org/aspectj/testing/run/WrappedRunIterator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/AccumulatingFileFilter.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/BridgeUtil.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/CollectorFileFilter.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/Diffs.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/FileUtil.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/IntRange.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/IntValidator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/IteratorWrapper.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/LangUtil.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/LineReader.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/Node.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/NullPrintStream.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/ObjectChecker.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/ProxyPrintStream.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/RunUtils.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/SFileReader.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/StandardObjectChecker.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/StreamSniffer.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/StreamsHandler.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/StringAccumulator.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/StringVisitor.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/TestClassLoader.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/TestDiffs.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/ValidFileFilter.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/WebInstall.java [new file with mode: 0644]
testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java [new file with mode: 0644]
testing/src/org/aspectj/testing/xml/IXmlWritable.java [new file with mode: 0644]
testing/src/org/aspectj/testing/xml/SoftMessage.java [new file with mode: 0644]
testing/src/org/aspectj/testing/xml/SoftSourceLocation.java [new file with mode: 0644]
testing/src/org/aspectj/testing/xml/XMLWriter.java [new file with mode: 0644]
testing/testdata/harnessList.txt [new file with mode: 0644]
testing/testdata/suite.dtd [new file with mode: 0644]
testing/testdata/suite.xml [new file with mode: 0644]
testing/testsrc/TestingModuleTests.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/harness/bridge/AbstractRunSpecTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/harness/bridge/AjcSpecTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/harness/bridge/CompilerRunSpecTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/harness/bridge/ParseTestCase.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/harness/bridge/TestingBridgeTests.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/BridgeUtilTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/FileUtilTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/IteratorWrapperTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/LangUtilTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/MessageUtilTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/StreamGrabberTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/TestDiffsTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/UtilTests.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/xml/AjcSpecXmlReaderTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/xml/TestingXmlTests.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/xml/XMLWriterTest.java [new file with mode: 0644]

diff --git a/lib/commons/commons-src.zip b/lib/commons/commons-src.zip
new file mode 100644 (file)
index 0000000..9ac8d71
Binary files /dev/null and b/lib/commons/commons-src.zip differ
diff --git a/lib/commons/commons.jar b/lib/commons/commons.jar
new file mode 100644 (file)
index 0000000..e425497
Binary files /dev/null and b/lib/commons/commons.jar differ
index 33ce546519bfebf25947e795e1701582a849f2ab..1911ac0078ab24999bd05ccf482894a72d9d620d 100644 (file)
@@ -24,6 +24,11 @@ This module also contains some bootstrap libraries and test sources.
        </li>
    <li><a href="build">build</a>: bootstrap libraries for the build process.
        </li>
+   <li><a href="commons">commons</a>:
+       Commons 1.0 from 
+       <a ="http:jakarta.apache.org/commons">http:jakarta.apache.org/commons</a>.
+       This is used only by the testing module.
+       </li>
    <li><a href="eclipse2.0">eclipse2.0</a>:
        The AspectJ compiler is a patch to the Eclipse 2.0 compiler.
        These are the non-compiler binaries.  For the eclipse compiler
diff --git a/testing-client/.classpath b/testing-client/.classpath
new file mode 100644 (file)
index 0000000..116a952
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+    <classpathentry kind="src" path="src"/>
+    <classpathentry kind="src" path="testsrc"/>
+    <classpathentry kind="var" path="JRE_LIB" rootpath="JRE_SRCROOT" sourcepath="JRE_SRC"/>
+    <classpathentry kind="src" path="/bridge"/>
+    <classpathentry kind="src" path="/util"/>
+    <classpathentry kind="lib" path="/lib/junit/junit.jar" sourcepath="/lib/junit/junit-src.jar"/>
+    <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/testing-client/.project b/testing-client/.project
new file mode 100644 (file)
index 0000000..2f32f33
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>testing-client</name>
+       <comment></comment>
+       <projects>
+               <project>bridge</project>
+               <project>util</project>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/testing-client/src/.cvsignore b/testing-client/src/.cvsignore
new file mode 100644 (file)
index 0000000..a3f0b1b
--- /dev/null
@@ -0,0 +1 @@
+*.lst
diff --git a/testing-client/src/org/aspectj/testing/Tester.java b/testing-client/src/org/aspectj/testing/Tester.java
new file mode 100644 (file)
index 0000000..66aae94
--- /dev/null
@@ -0,0 +1,983 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing; // XXX move to its own client package
+
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.Message;
+import org.aspectj.util.LangUtil;
+
+import java.util.*;
+import java.io.*;
+
+/**
+ * Testing client interface for checking results and reporting
+ * to a delegate IMessageHandler.
+ * Harnesses providing this interface for test clients must 
+ * set it up by calling
+ * {@link #setBASEDIR(File)}
+ * {@link #setMessageHandler(IMessageHandler)} and 
+ * {@link #clear()} for each test, as appropriate.
+ * (That means that IMessageHandler must be loaded from a class
+ * loader common to the harness and Tester.)
+ * If clients submit a failing check, this registers the message
+ * and throws an AbortException holding the message; this 
+ * AbortException <b>will not</b> have the correct stack trace;
+ * all the information should be encoded in the message.
+ * Find any original exception thrown in the message itself.
+ */
+  // XXX consider creating exception for new API throwFailure(String m) 
+public class Tester {
+    /** delegate for reporting results */
+    private static IMessageHandler messageHandler;
+    
+    /** base directory for calculating relative paths to event files */
+    private static File BASEDIR;
+    
+    /** 
+     * collection of notes submitted
+     */
+    private static Set notes;
+    
+    /** <code>List</code> to hold events submitted. */
+    private static List actualEvents = new ArrayList();
+    
+    /** <code>List</code> to hold events we expect. */
+    private static List expectedEvents = new ArrayList();
+
+    static {
+        setBASEDIR(new File("."));
+        setMessageHandler(IMessageHandler.SYSTEM_ERR);
+        clear();
+    }
+
+    /**
+     * Set directory used for calculating relative paths
+     * (currently only to an events file)
+     * @param baseDir the File for an existing directory
+     */
+    public static void setBASEDIR(File baseDir) {
+        if (null == baseDir) throw new IllegalArgumentException("null baseDir");
+        if (!baseDir.isDirectory()) throw new IllegalArgumentException("not a directory: " + baseDir);
+        BASEDIR = baseDir;
+    }
+
+    public static File getBASEDIR() {
+        return BASEDIR;
+    }
+    
+    
+    /**
+     * Set the message handler used for this Tester.
+     * When given a message of kind FAIL, this handler 
+     * must complete abruptly or return false (i.e., not handled completely)
+     * so the Tester throws an AbortException.  
+     * @see checkFailed(..).
+     */
+    public static void setMessageHandler(IMessageHandler handler) {   
+        if (null == handler) throw new IllegalArgumentException("null handler");
+        if (messageHandler != handler) messageHandler = handler;
+    }
+
+    
+    public static void clear() {
+        clearNotes();
+        clearEvents();
+    }
+
+    /** XXX deprecated #clear() */
+    public static void clearNotes() {
+        notes = new HashSet();
+    }
+
+    /** XXX deprecated #clear() */
+    public static void clearEvents() { 
+        actualEvents = new ArrayList(); 
+        expectedEvents = new ArrayList();     
+    }
+    
+
+    /** Add an actual event */
+    public static void event(String s) { 
+        actualEvents.add(s); 
+    }
+    
+    /**
+     * Add a note to {@link #notes}.
+     * @param note Message to add.
+     * XXX deprecated event(String) 
+     */
+    public static void note(Object note) { 
+        notes.add(note);
+    }
+
+    /**
+     * Checks that <code>note</code> was added using {@link #note},
+     * and fails using <code>note.toString()</code> is it wasn't found.
+     *
+     * @param note Message that should've been added using {@link #note}.
+     * XXX deprecated checkEvent(String) 
+     */
+    public static void check(Object note) {
+        check(note, "expected note \"" + note.toString() + "\"");
+    }
+
+    /**
+     * Checks that <code>note</code> was added using {@link #note},
+     * and fails using <code>message</code> is it wasn't found.
+     *
+     * @param note    Message that should've been added using {@link #note}.
+     * @param message Message with which to fail if <code>node</code>
+     *                wasn't added.
+     */
+    public static void check(Object note, String message) {
+        check(notes.contains(note), message);
+    }
+
+    /**
+     * Reports that <code>t</code> shouldn't have been thrown.
+     * using <code>t</code> as the message.
+     *
+     * @param t Thrown exception.
+     * @see #throwable(Throwable,String)
+     */
+    public static void throwable(Throwable t) {
+        throwable(t, null);
+    }
+
+    
+    /**
+     * Reports that <code>t</code> shouldn't have been thrown.
+     * using <code>msg</code> as the message.
+     *
+     * @param thrown   Thrown exception.
+     * @param msg Message with which to report error.
+     */
+    public static void throwable(Throwable thrown, String msg) {
+        handle(msg, thrown, true);
+   }
+
+    /**
+     * Report the error <code>message</code> unconditionally.
+     *
+     * @param message Error to report.
+     */
+    public static void checkFailed(String message) {
+        handle(message, null, true);
+    }
+
+    /**
+     * Check that <code>expectedNotes</code> is equal to {@link #notes}
+     * , fail with <code>msg</code> and create a new instance of {@link #notes}.
+     * <i>NOTE: <code>expectedNotes</code> is a <code>String</code>, so
+     * it must match with {@link java.util.HashSet#toString()}</i>.
+     *
+     * @param expectedNotes <code>String</code> we expect
+     *                      {@link #notes} to match.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkAndClear(String expectedNotes, String msg) {
+        checkEqual(notes, expectedNotes, msg);
+        clearNotes();
+    }
+
+    /**
+     * Reports an error using <code>message</code> if
+     * <code>test == false</code>.
+     *
+     * @param test    Determines whether we call {@link #checkFailed}.
+     * @param message Message to pass {@link #checkFailed} if
+     *                <code>test == false</code>.
+     */
+    public static void check(boolean test, String message) {
+        if (!test) checkFailed(message);
+    }
+
+    /**
+     * Checks that the values of <code>value</code> and
+     * <code>expectedValue</code> are equal.  Both or either
+     * can be null. Calls {@link #checkFailed} with <code>message</code>
+     * if the arrays aren't equal.
+     *
+     * @param value         One test set.
+     * @param expectedValue The other test set.
+     * @param message       Message with which to fail.
+     */
+    public static void checkEqual(Object[] value,
+                                  Object[] expectedValue,
+                                  String message)
+    {
+        if (value == null) {
+            if (expectedValue == null) return;
+            checkFailed(message+" null array found");
+            return;
+        }
+        int n = value.length;
+        if (n != expectedValue.length) {
+            checkFailed(message+" expected array of length "+n
+                                +" got "+expectedValue.length);
+            return;
+        }
+        for(int i=0; i<n; i++) {
+            if (!value[i].equals(expectedValue[i])) {
+                checkFailed(message+": "+value[i]+" != "+
+                            expectedValue[i]+" at index "+i);
+            }
+        }
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(boolean s, boolean t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(boolean s, boolean t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(byte s, byte t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(byte s, byte t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(char s, char t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(char s, char t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(short s, short t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(short s, short t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(int s, int t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(int s, int t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(long s, long t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(long s, long t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(float s, float t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(float s, float t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(double s, double t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(double s, double t, String msg) {
+        if (s == t) checkFailed(msg);
+    }
+
+    /**
+     * Fails if <code>s == t</code>.
+     *
+     * @param s a known value.
+     * @param t another known value.
+     */
+    public static void checkNotEqual(Object s, Object t) {
+        checkNotEqual(s, t, s + " shouldn't equal " + t);
+    }
+    /**
+     * Fails with message <code>msg</code> if <code>s == t</code>
+     * or both <code>s</code> and <code>t</code> are <code>null</code>.
+     *
+     * @param s   a known value.
+     * @param t   another known value.
+     * @param msg the failure message.
+     */
+    public static void checkNotEqual(Object s, Object t, String msg) {
+        if ((s != null && s.equals(t)) ||
+            (t != null && t.equals(s)) ||
+            (s == null && t == null)) {
+            checkFailed(msg);
+        }
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(int,int,String)
+     */
+    public static void checkEqual(int value, int expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }
+
+    /**
+     * Fails if the passed in value is <code>null</code>.
+     *
+     * @param o    the expected non-null thing.
+     * @param name the name of <code>o</code>.
+     */
+    public static void checkNonNull(Object o, String name) {
+        if (o == null) checkFailed(name + " shouldn't be null");
+    }
+
+    /**
+     * Compared <code>value</code> and <code>expectedValue</code>
+     * and fails with <code>message</code> if they aren't equal.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(int value, int expectedValue, String message) {
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            message = message+": "+value+" < "+expectedValue;
+        } else {
+            message = message+": "+value+" > "+expectedValue;
+        }
+        checkFailed(message);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(float,float,String)
+     */
+    public static void checkEqual(float value, float expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }
+
+    /**
+     * Compared <code>value</code> and <code>expectedValue</code>
+     * and fails with <code>message</code> if they aren't equal.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(float value, float expectedValue, String msg) {
+        if (Float.isNaN(value) && Float.isNaN(expectedValue)) return;
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            msg = msg+": "+value+" < "+expectedValue;
+        } else {
+            msg = msg+": "+value+" > "+expectedValue;
+        }
+        checkFailed(msg);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(long,long,String)
+     */
+    public static void checkEqual(long value, long expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }    
+
+    /**
+     * Compared <code>value</code> and <code>expectedValue</code>
+     * and fails with <code>message</code> if they aren't equal.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(long value, long expectedValue, String msg) {
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            msg = msg+": "+value+" < "+expectedValue;
+        } else {
+            msg = msg+": "+value+" > "+expectedValue;
+        }
+        checkFailed(msg);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(double,double,String)
+     */
+    public static void checkEqual(double value, double expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }       
+
+    /**
+     * Compared <code>value</code> and <code>expectedValue</code>
+     * and fails with <code>message</code> if they aren't equal.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(double value, double expectedValue, String msg) {
+        if (Double.isNaN(value) && Double.isNaN(expectedValue)) return;
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            msg = msg+": "+value+" < "+expectedValue;
+        } else {
+            msg = msg+": "+value+" > "+expectedValue;
+        }
+        checkFailed(msg);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(short,short,String)
+     */
+    public static void checkEqual(short value, short expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }    
+
+    /**
+     * Compared <code>value</code> and <code>expectedValue</code>
+     * and fails with <code>message</code> if they aren't equal.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(short value, short expectedValue, String msg) {
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            msg = msg+": "+value+" < "+expectedValue;
+        } else {
+            msg = msg+": "+value+" > "+expectedValue;
+        }
+        checkFailed(msg);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(byte,byte,String)
+     */
+    public static void checkEqual(byte value, byte expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }    
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>msg</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(byte value, byte expectedValue, String msg) {
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            msg = msg+": "+value+" < "+expectedValue;
+        } else {
+            msg = msg+": "+value+" > "+expectedValue;
+        }
+        checkFailed(msg);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(char,char,String)
+     */
+    public static void checkEqual(char value, char expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }    
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>msg</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(char value, char expectedValue, String msg) {
+        if (value == expectedValue) return;
+        if (value < expectedValue) {
+            msg = msg+": "+value+" < "+expectedValue;
+        } else {
+            msg = msg+": "+value+" > "+expectedValue;
+        }
+        checkFailed(msg);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(boolean,boolean,String)
+     */
+    public static void checkEqual(boolean value, boolean expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }    
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>msg</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @param msg           Message with which to fail.
+     */
+    public static void checkEqual(boolean value, boolean expectedValue, String msg) {
+        if (value == expectedValue) return;
+        msg = msg+": "+value+" != "+expectedValue;
+        checkFailed(msg);
+    }
+
+    /**
+     * Checks whether the entries of <code>set</code> are equal
+     * using <code>equals</code> to the corresponding String in
+     * <code>expectedSet</code> and fails with message <code>msg</code>.
+     *
+     * @param set         Unkown set of values.
+     * @param expectedSet Expected <code>String</code> of values.
+     * @param msg         Message with which to fail.
+     */
+    public static void checkEqual(Collection set, String expectedSet, String msg) {
+        checkEqual(set, LangUtil.split(expectedSet), msg);
+    }
+        
+    /**
+     * Checks whether the entries of <code>set</code> are equal
+     * using <code>equals</code> to the corresponding entry in
+     * <code>expectedSet</code> and fails with message <code>msg</code>,
+     * except that duplicate actual entries are ignored.
+     * This issues fail messages for each failure; when 
+     * aborting on failure, only the first will be reported.
+     *
+     * @param set         Unkown set of values.
+     * @param expectedSet Expected <code>String</code> of values.
+     * @param msg         Message with which to fail.
+     */
+    public static void checkEqualIgnoreDups(Collection set, String[] expected, String msg,
+                                              boolean ignoreDups) {
+        String[] diffs = diffIgnoreDups(set, expected, msg, ignoreDups);
+        if (0 < diffs.length) {
+            check(false, "" + Arrays.asList(diffs));
+        }
+//        for (int i = 0; i < diffs.length; i++) {
+//                     check(false, diffs[i]);
+//             }                                            
+    }
+    
+    /** @return String[] of differences '{un}expected msg "..." {not} found' */
+    private static String[] diffIgnoreDups(Collection set, String[] expected, String msg, 
+        boolean ignoreDups) {
+        ArrayList result = new ArrayList();
+        ArrayList actual = new ArrayList(set);
+        BitSet hits = new BitSet();
+        for (int i = 0; i < expected.length; i++) {
+            if (!actual.remove(expected[i])) {
+                result.add(" expected " + msg + " \"" + expected[i] + "\" not found");
+            } else {
+                hits.set(i);
+                if (ignoreDups) {
+                   while (actual.remove(expected[i])) ; // remove all instances of it
+                }
+            }
+        }
+        for (Iterator iter = actual.iterator(); iter.hasNext();) {
+                       String act = (String) iter.next();
+            result.add(" unexpected " + msg + " \"" + act + "\" found");                       
+               }
+        return (String[]) result.toArray(new String[0]);
+    }
+
+    /**
+     * Checks whether the entries of <code>set</code> are equal
+     * using <code>equals</code> to the corresponding entry in
+     * <code>expectedSet</code> and fails with message <code>msg</code>.
+     *
+     * @param set         Unkown set of values.
+     * @param expectedSet Expected <code>String</code> of values.
+     * @param msg         Message with which to fail.
+     */
+    public static void checkEqual(Collection set, String[] expected, String msg) {
+        checkEqualIgnoreDups(set, expected, msg, false);
+    }
+
+    /**
+     * Compares <code>value</code> and <code>expectedValue</code>
+     * with failing message <code>"compare"</code>.
+     *
+     * @param value         Unkown value.
+     * @param expectedValue Expected value.
+     * @see   #checkEqual(Object,Object,String)
+     */
+    public static void checkEqual(Object value, Object expectedValue) {
+        checkEqual(value, expectedValue, "compare");
+    }    
+
+    /**
+     * Checks whether the entries of <code>set</code> are equal
+     * using <code>equals</code> to the corresponding String in
+     * <code>expectedSet</code> and fails with message <code>msg</code>.
+     *
+     * @param set         Unkown set of values.
+     * @param expectedSet Expected <code>String</code> of values.
+     * @param msg         Message with which to fail.
+     */
+    public static void checkEqual(Object value, Object expectedValue, String msg) {
+        if (value == null && expectedValue == null) return;
+        if (value != null && value.equals(expectedValue)) return;
+        msg = msg+": "+value+" !equals "+expectedValue;
+        checkFailed(msg);
+    }
+
+    /**
+     * Checks whether the entries of <code>set</code> are equal
+     * using <code>equals</code> to the corresponding String in
+     * <code>expectedSet</code> and fails with message <code>msg</code>.
+     *
+     * @param set         Unkown set of values.
+     * @param expectedSet Expected <code>String</code> of values.
+     * @param msg         Message with which to fail.
+     */
+    public static void checkEq(Object value, Object expectedValue, String msg) {
+        if (value == expectedValue) return;
+        msg = msg+": "+value+" != "+expectedValue;
+        checkFailed(msg);
+    }
+
+     /** add expected events */
+    public static void expectEvent(String s) {
+        if (null != s) {
+            expectedEvents.add(s);
+        }
+    }
+    
+     /** add expected events */
+    public static void expectEvent(Object s) {
+        if (null != s) {
+            expectEvent(s.toString());
+        }
+    }
+    
+    /**
+     * add expected events, parse out ; from string
+     * Expect those messages in <code>s</code> separated by
+     * <code>":;, "</code>.
+     *
+     * @param s String containg delimited,expected messages.
+     */
+    public static void expectEventsInString(String s) {
+        if (null != s) {
+            StringTokenizer tok = new StringTokenizer(s, ":;, ");
+            while (tok.hasMoreTokens()) {
+                expectEvent(tok.nextToken());
+            }
+        }
+    }
+
+    public static void expectEventsInString(String[] ra) {
+        expectEvents((Object[]) ra);
+    }
+
+   /** add expected events */
+    public static void expectEvents(Object[] events) {
+        if (null != events) {
+            for (int i = 0; i < events.length; i++) {
+                               if (null != events[i]) {
+                    expectEvent(events[i].toString());
+                }
+                       }
+        }
+    }
+
+    /** add expected events */
+    public static void expectEvents(String[] events) {
+        if (null != events) {
+            for (int i = 0; i < events.length; i++) {
+                if (null != events[i]) {
+                    expectEvent(events[i].toString());
+                }
+            }            
+        }
+    }
+    
+    /** check actual and expected have same members */
+    public static void checkAllEvents() {
+        checkAndClearEvents((String[]) expectedEvents.toArray(new String[0]));
+    }
+    
+    /** also ignore duplicate actual entries for expected */
+    public static void checkAllEventsIgnoreDups() {
+        final boolean ignoreDups = true;
+        final String[] exp = (String[]) expectedEvents.toArray(new String[0]);
+        checkEqualIgnoreDups(actualEvents, exp, "event", ignoreDups);
+        clearEvents();
+    }
+    
+    /** Check events, file is line-delimited.  If there is a non-match, signalls
+     * a single error for the first event that does not match the next event in 
+     * the file.  The equivalence is {@link #checkEqualLists}.  Blank lines are 
+     * ignored.  lines that start with '//' are ignored.  */
+    public static void checkEventsFromFile(String eventsFile) {
+        // XXX bug reads into current expected and checks all - separate read and check
+        try {
+            File file = new File(getBASEDIR(), eventsFile);   // XXX TestDriver
+            BufferedReader in = new BufferedReader(new FileReader(file));
+            //final File parentDir = (null == file? null : file.getParentFile());
+            String line;
+            List expEvents = new ArrayList();
+            while ((line = in.readLine()) != null) {
+                line = line.trim();
+                if ((line.length() < 1) || (line.startsWith("//"))) continue;
+                expEvents.add(line);
+            }
+            checkEqualLists(actualEvents, expEvents, " from " + eventsFile);
+        } catch (IOException ioe) {
+            throwable(ioe);
+        }
+    }
+    
+    
+    /** Check to see that two lists of strings are the same.  Order is important.
+     *  Trimmable whitespace is not important.  Case is important.
+     *
+     *         @param actual one list to check
+     *  @param expected another list
+     *  @param message a context string for the resulting error message if the test fails.
+     */
+    public static void checkEqualLists(List/*String*/ actual, List/*String*/ expected, 
+                                       String message) {
+               Iterator a = actual.iterator();
+               Iterator e = expected.iterator();
+        int ai = 0;
+        int ei = 0;
+               for (; a.hasNext(); ) {
+                   if (! e.hasNext()) {
+                       checkFailed("unexpected [" + ai + "] \"" + a.next() + "\" " + message);
+                       return;
+                   }
+                   String a0 = ((String) a.next()).trim();
+                   String e0 = ((String) e.next()).trim();
+                   if (! a0.equals(e0)) {
+                       checkFailed("expected [" + ei + "] \"" + e0 
+                        + "\"\n  but found [" + ai + "] \"" + a0 + "\"\n  " + message);
+                       return;
+                   }
+            ai++;
+            ei++;
+               }
+               while (e.hasNext()) {
+                    checkFailed("expected [" + ei + "] \"" + e.next() + "\" " + message);
+             ei++;
+               }
+    }      
+           
+    /** Check events, expEvents is space delimited */
+    public static void checkEvents(String expEvents) {
+        checkEqual(actualEvents, expEvents, "event");
+    }
+
+    /** Check events, expEvents is an array */
+    public static void checkEvents(String[] expEvents) {
+        checkEqual(actualEvents, expEvents, "event");
+    }
+        
+    /** Check events and clear after check*/
+    public static void checkAndClearEvents(String expEvents) {
+        checkEvents(expEvents);
+        clearEvents();
+    }    
+    
+    /** Check events and clear after check*/
+    public static void checkAndClearEvents(String[] expEvents) {
+        checkEvents(expEvents);
+        clearEvents();
+    }
+
+    /** XXX deprecated */
+    public static void printEvents() {  // XXX no clients?
+        for (Iterator i = actualEvents.iterator(); i.hasNext(); ) {
+            System.out.println(i.next());               // XXX System.out
+        }
+    }
+
+    /**
+     * Report an uncaught exeption as an error
+     * @param thrown <code>Throwable</code> to print.
+     * @see   #maxStackTrace
+     */
+    public void unexpectedExceptionFailure(Throwable thrown) { 
+        handle("unexpectedExceptionFailure", thrown, true);
+    }
+    
+    /**
+     * Handle message by delegation to message handler, doing
+     * IMessage.FAIL if (fail || (thrown != null) and IMessage.INFO 
+     * otherwise.
+     */
+
+    private static void handle(String message, Throwable thrown, boolean fail) {
+        final boolean failed = fail || (null != thrown);
+        IMessage.Kind kind = (failed ? IMessage.FAIL : IMessage.INFO);
+        IMessage m = new Message(message, kind, thrown, null);
+        final boolean handled = messageHandler.handleMessage(m);
+    }
+//    private static void resofhandle(String message, Throwable thrown, boolean fail) {
+//     /* If FAIL and the message handler returns false (normally),
+//     * Then this preserves "abort" semantics by throwing an
+//     * abort exception.  
+//     */
+//        if (failed) {
+//            if (handled) {
+//                String s = "Tester expecting handler to return false or "
+//                            + "complete abruptly when passed a fail, for " + m;
+//                m = new Message(s, IMessage.DEBUG, null, null);
+//                messageHandler.handleMessage(m);
+//            } else {
+//                throw AbortException.borrowPorter(m);
+//            }
+//        }
+//    }
+
+}
diff --git a/testing-client/testsrc/TestingClientModuleTests.java b/testing-client/testsrc/TestingClientModuleTests.java
new file mode 100644 (file)
index 0000000..7148e03
--- /dev/null
@@ -0,0 +1,31 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+// default package
+
+import junit.framework.*;
+import junit.framework.Test;
+
+public class TestingClientModuleTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(TestingClientModuleTests.class.getName());
+        suite.addTestSuite(TestingClientModuleTests.class); // minimum 1 test (testNothing)
+        return suite;
+    }
+
+    public TestingClientModuleTests(String name) { super(name); }
+
+    public void testNothing() {}
+}  
diff --git a/testing-util/.classpath b/testing-util/.classpath
new file mode 100644 (file)
index 0000000..c6752e3
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+    <classpathentry kind="src" path="src"/>
+    <classpathentry kind="src" path="testsrc"/>
+    <classpathentry kind="var" path="JRE_LIB" rootpath="JRE_SRCROOT" sourcepath="JRE_SRC"/>
+    <classpathentry kind="lib" path="/lib/junit/junit.jar" sourcepath="/lib/junit/junit-src.jar"/>
+    <classpathentry kind="src" path="/util"/>
+    <classpathentry kind="lib" path="/lib/jdiff/jdiff.jar"/>
+    <classpathentry kind="src" path="/bridge"/>
+    <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/testing-util/.project b/testing-util/.project
new file mode 100644 (file)
index 0000000..917eccb
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>testing-util</name>
+       <comment></comment>
+       <projects>
+               <project>bridge</project>
+               <project>util</project>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/testing-util/TestUtilTest.launch b/testing-util/TestUtilTest.launch
new file mode 100644 (file)
index 0000000..645c3ee
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
+    <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.aspectj.testing.util.TestUtilTest"/>
+    <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+    <booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+    <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="testing-util"/>
+    <listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry containerPath=&quot;JRE_LIB&quot; path=&quot;2&quot;
+    sourceAttachmentPath=&quot;JRE_SRC&quot; sourceRootPath=&quot;JRE_SRCROOT&quot; type=&quot;3&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;testing-util&quot; type=&quot;1&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry internalArchive=&quot;/lib/junit/junit.jar&quot; path=&quot;3&quot;
+    sourceAttachmentPath=&quot;/lib/junit/junit-src.jar&quot; type=&quot;2&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;util&quot; type=&quot;1&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry internalArchive=&quot;/lib/jdiff/jdiff.jar&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;bridge&quot; type=&quot;1&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;bcweaver&quot; type=&quot;1&quot;/&gt;
+"/>
+        <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;runtimeClasspathEntry internalArchive=&quot;/lib/bcel/bcel.jar&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;
+"/>
+    </listAttribute>
+    <stringAttribute key="org.eclipse.debug.ui.target_run_perspective" value="perspective_default"/>
+    <stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.debug.ui.javaSourceLocator"/>
+    <stringAttribute key="org.eclipse.debug.ui.target_debug_perspective" value="perspective_default"/>
+</launchConfiguration>
diff --git a/testing-util/src/.cvsignore b/testing-util/src/.cvsignore
new file mode 100644 (file)
index 0000000..a3f0b1b
--- /dev/null
@@ -0,0 +1 @@
+*.lst
diff --git a/testing-util/src/org/aspectj/testing/util/TestUtil.java b/testing-util/src/org/aspectj/testing/util/TestUtil.java
new file mode 100644 (file)
index 0000000..b33e1fa
--- /dev/null
@@ -0,0 +1,616 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+import org.aspectj.util.Reflection;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import jdiff.text.FileLine;
+import jdiff.util.Diff;
+import jdiff.util.DiffNormalOutput;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+import junit.runner.TestCaseClassLoader;
+
+/** 
+ * Things that junit should perhaps have, but doesn't.
+ * Note the file-comparison methods require JDiff to run,
+ * but JDiff types are not required to resolve this class.
+ * Also, the bytecode weaver is required to compare class
+ * files, but not to compare other files.
+ */
+
+public final class TestUtil {
+
+    private TestUtil() {
+        super();
+    }
+
+    // ---- arrays
+
+    public static void assertArrayEquals(String msg, Object[] expected, Object[] found) {
+        TestCase.assertEquals(msg, Arrays.asList(expected), Arrays.asList(found));
+    }
+
+    // ---- unordered
+
+    public static void assertSetEquals(Collection expected, Collection found) {
+        assertSetEquals(null, expected, found);
+    }
+
+    public static void assertSetEquals(String msg, Object[] expected, Object[] found) {
+        assertSetEquals(msg, Arrays.asList(expected), Arrays.asList(found));
+    }
+
+    public static void assertSetEquals(
+        String msg,
+        Collection expected,
+        Collection found) {
+        msg = (msg == null) ? "" : msg + ": ";
+
+        Set results1 = new HashSet(found);
+        results1.removeAll(expected);
+
+        Set results2 = new HashSet(expected);
+        results2.removeAll(found);
+
+        if (results1.isEmpty()) {
+            TestCase.assertTrue(
+                msg + "Expected but didn't find: " + results2.toString(),
+                results2.isEmpty());
+        } else if (results2.isEmpty()) {
+            TestCase.assertTrue(
+                msg + "Didn't expect: " + results1.toString(),
+                results1.isEmpty());
+        } else {
+            TestCase.assertTrue(
+                msg
+                    + "Expected but didn't find: "
+                    + results2.toString()
+                    + "\nDidn't expect: "
+                    + results1.toString(),
+                false);
+        }
+    }
+
+    // ---- objects
+
+    public static void assertCommutativeEquals(Object a, Object b, boolean should) {
+        TestCase.assertEquals(a + " equals " + b, should, a.equals(b));
+        TestCase.assertEquals(b + " equals " + a, should, b.equals(a));
+        assertHashEquals(a, b, should);
+    }
+
+    private static void assertHashEquals(Object s, Object t, boolean should) {
+        if (should) {
+            TestCase.assertTrue(
+                s + " does not hash to same as " + t,
+                s.hashCode() == t.hashCode());
+        } else {
+            if (s.hashCode() == t.hashCode()) {
+                System.err.println("warning: hash collision with hash = " + t.hashCode());
+                System.err.println("  for " + s);
+                System.err.println("  and " + t);
+            }
+        }
+    }
+    
+    // -- reflective stuff
+
+       public static void runMain(String classPath, String className) {
+               runMethod(classPath, className, "main", new Object[] { new String[0] });
+       }
+
+
+       public static Object runMethod(String classPath, String className, String methodName, Object[] args) {
+               classPath += File.pathSeparator + System.getProperty("java.class.path");
+               
+               ClassLoader loader = new TestCaseClassLoader(classPath);
+               
+               Class c=null;
+               try {
+                       c = loader.loadClass(className);
+               } catch (ClassNotFoundException e) {
+                       Assert.assertTrue("unexpected exception: " + e, false);
+               }
+               return Reflection.invokestaticN(c, methodName, args);
+       }
+
+               
+       /**
+        * Checks that two multi-line strings have the same value.
+        * Each line is trimmed before comparision
+        * Produces an error on the particular line of conflict
+        */    
+    public static void assertMultiLineStringEquals(String message, String s1, String s2) {
+       try {
+               BufferedReader r1 = new BufferedReader(new StringReader(s1));
+               BufferedReader r2 = new BufferedReader(new StringReader(s2));
+               
+               
+               List lines = new ArrayList();
+               String l1, l2;
+               
+               int index = 1;
+               while(true) {
+                   l1 = readNonBlankLine(r1);
+                   l2 = readNonBlankLine(r2);
+                   if (l1 == null || l2 == null) break;
+                   if (l1.equals(l2)) {
+                       lines.add(l1);
+                   } else {
+                       showContext(lines);
+                       Assert.assertEquals(message +"(line " + index +")", l1, l2);
+                   }
+                   index++;
+               }
+               if (l1 != null) showContext(lines);
+               Assert.assertTrue(message + ": unexpected " + l1, l1 == null);
+               if (l2 != null) showContext(lines);
+               Assert.assertTrue(message + ": unexpected " + l2, l2 == null);
+       } catch (IOException ioe) {
+               Assert.assertTrue(message + ": caught " + ioe.getMessage(), false);
+       }
+    }
+    
+       private static void showContext(List lines) {
+               int n = lines.size();
+               for (int i = Math.max(0, n - 8); i < n; i++) {
+                       System.err.println(lines.get(i));
+               }
+       }       
+       
+       private static String readNonBlankLine(BufferedReader r) throws IOException {
+               String l = r.readLine();
+               if (l == null) return null;
+        l = l.trim();
+        // comment to include comments when reading
+        int commentLoc = l.indexOf("//");
+        if (-1 != commentLoc) {
+            l = l.substring(0, commentLoc).trim();
+        }
+               if ("".equals(l)) return readNonBlankLine(r);
+        return l;
+       }
+    
+    /**
+     * If there is an expected dir, expect each file in its subtree
+     * to match a corresponding actual file in the base directory.
+     * @return boolean
+     */
+    public static boolean sameDirectoryContents(
+        final IMessageHandler handler, 
+        final File expectedBaseDir,
+        final File actualBaseDir,
+        final boolean fastFail) {
+        LangUtil.throwIaxIfNull(handler, "handler");
+        FileUtil.throwIaxUnlessCanReadDir(actualBaseDir, "actualBaseDir");
+        if (!FileUtil.canReadDir(expectedBaseDir)) {
+            MessageUtil.fail(handler, " expected dir not found: " + expectedBaseDir);
+            return false;
+        }
+        if (!FileUtil.canReadDir(actualBaseDir)) {
+            MessageUtil.fail(handler, " actual dir not found: " + actualBaseDir);
+            return false;
+        }
+        String[] paths = FileUtil.listFiles(expectedBaseDir);
+        boolean result = true;
+        for (int i = 0; i < paths.length; i++) {
+            if (!sameFiles(handler, expectedBaseDir, actualBaseDir, paths[i]) && !result) {
+                result = false;
+                if (fastFail) {
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+    
+    //------------ File-comparison utilities (XXX need their own class...)
+    /**
+     * Compare two files, line by line, and report differences as one FAIL message
+     * if a handler is supplied.  This preprocesses .class files by disassembling.
+     * @param handler the IMessageHandler for any FAIL messages (null to ignore)
+     * @param expectedFile the File path to the canonical file
+     * @param actualFile the File path to the actual file, if any
+     * @return true if the input files are the same, based on per-line comparisons
+     */
+    public static boolean sameFiles (
+        IMessageHandler handler,
+        File expectedFile, 
+        File actualFile) {
+        return doSameFile(handler, null, null, expectedFile, actualFile);
+    }
+
+    /**
+     * Compare two files, line by line, and report differences as one FAIL message
+     * if a handler is supplied.  This preprocesses .class files by disassembling.
+     * This method assumes that the files are at the same offset from two
+     * respective base directories.
+     * @param handler the IMessageHandler for any FAIL messages (null to ignore)
+     * @param expectedBaseDir the File path to the canonical file base directory
+     * @param actualBaseDir the File path to the actual file base directory
+     * @param path the String path offset from the base directories
+     * @return true if the input files are the same, based on per-line comparisons
+     */
+    public static boolean sameFiles (
+        IMessageHandler handler,
+        File expectedBaseDir, 
+        File actualBaseDir, 
+        String path) {
+        File actualFile = new File(actualBaseDir, path);
+        File expectedFile = new File(expectedBaseDir, path);
+        return doSameFile(handler, expectedBaseDir, actualBaseDir, expectedFile, actualFile);
+    }
+
+    /**
+     * This does the work, selecting a lineator subclass and converting public
+     * API's to JDiff APIs for comparison.
+     * Currently, all jdiff interfaces are method-local, so this class with load 
+     * without it; if we do use it, we can avoid the duplication.
+     */
+    private static boolean doSameFile(
+        IMessageHandler handler,
+        File expectedBaseDir, 
+        File actualBaseDir,
+        File expectedFile,
+        File actualFile) {
+        String path = expectedFile.getPath();
+        // XXX permit user to specify lineator
+        ILineator lineator = Lineator.TEXT;
+        if (path.endsWith(".class")) {
+            if (ClassLineator.haveDisassembler() ) {
+                lineator = Lineator.CLASS;
+            } else {
+                MessageUtil.abort(handler, "skipping - dissassembler not available");
+                return false;
+            }
+        }
+        CanonicalLine[] actualLines = null;
+        CanonicalLine[] expectedLines = null;
+        try {
+            actualLines = lineator.getLines(handler, actualFile, actualBaseDir); 
+            expectedLines = lineator.getLines(handler, expectedFile, expectedBaseDir);
+        } catch (IOException e) {
+            MessageUtil.fail(handler, "rendering lines ", e);
+            return false;
+        }
+        if (!LangUtil.isEmpty(actualLines) && !LangUtil.isEmpty(expectedLines)) {
+            // here's the transmutation back to jdiff - extract if publishing JDiff
+            CanonicalLine[][] clines = new CanonicalLine[][] { expectedLines, actualLines };
+            FileLine[][] flines = new FileLine[2][];
+            for (int i = 0; i < clines.length; i++) {
+                CanonicalLine[] cline = clines[i];
+                FileLine[] fline = new FileLine[cline.length];
+                for (int j = 0; j < fline.length; j++) {
+                                       fline[j] = new FileLine(cline[j].canonical, cline[j].line);
+                               }
+                flines[i] = fline;
+            }
+
+            Diff.change edits = new Diff(flines[0], flines[1]).diff_2(false);
+            if ((null == edits) || (0 == (edits.inserted + edits.deleted))) {
+                // XXX confirm with jdiff that null means no edits
+                return true;
+            } else {
+                //String m = render(handler, edits, flines[0], flines[1]);
+                StringWriter writer = new StringWriter();
+                DiffNormalOutput out = new DiffNormalOutput(flines[0], flines[1]);
+                out.setOut(writer);
+                out.setLineSeparator(LangUtil.EOL);
+                try {
+                    out.writeScript(edits);
+                } catch (IOException e) {
+                    MessageUtil.fail(handler, "rendering edits", e);
+                } finally {
+                    if (null != writer) {
+                        try { writer.close(); } 
+                        catch (IOException e) {
+                            MessageUtil.fail(handler, "closing after rendering edits", e);
+                        }
+                    }
+                }
+                String message = "diff between " 
+                    + path 
+                    + " in expected dir " 
+                    + expectedBaseDir
+                    + " and actual dir " 
+                    + actualBaseDir 
+                    + LangUtil.EOL
+                    + writer.toString(); 
+                MessageUtil.fail(handler, message);
+            }
+        }
+        return false;
+    }
+    
+
+    /** component that reduces file to CanonicalLine[] */
+    public static interface ILineator {
+        /** Lineator suitable for text files */
+        public static final ILineator TEXT = new TextLineator();
+
+        /** Lineator suitable for class files (disassembles first) */
+        public static final ILineator CLASS = new ClassLineator();
+
+        /**
+         * Reduce file to CanonicalLine[].
+         * @param handler the IMessageHandler for errors (may be null) 
+         * @param file the File to render
+         * @param basedir the File for the base directory (may be null) 
+         * @return CanonicalLine[] of lines - not null, but perhaps empty
+         */
+        CanonicalLine[] getLines(
+            IMessageHandler handler, 
+            File file, 
+            File basedir) throws IOException;
+    }
+    
+    /** alias for jdiff FileLine to avoid client binding */
+    public static class CanonicalLine {
+        public static final CanonicalLine[] NO_LINES = new CanonicalLine[0];
+        
+        /** canonical variant of line for comparison */
+        public final String canonical;
+        
+        /** actual line, for logging */
+        public final String line;
+        public CanonicalLine(String canonical, String line) {
+            this.canonical = canonical;
+            this.line = line;
+        }
+        public String toString() {
+            return line;
+        }
+    }
+    
+    private abstract static class Lineator implements ILineator {
+        /**
+         * Reduce file to CanonicalLine[].
+         * @param handler the IMessageHandler for errors (may be null) 
+         * @param file the File to render
+         * @param basedir the File for the base directory (may be null) 
+         */
+        public CanonicalLine[] getLines(
+            IMessageHandler handler, 
+            File file, 
+            File basedir) 
+            throws IOException {
+        
+            if (!file.canRead() || !file.isFile()) {
+                MessageUtil.error(handler, "not readable file: " + basedir + " - " + file);
+                return null;
+            }
+            // capture file as FileLine[]
+            InputStream in = null;
+            String path = FileUtil.normalizedPath(file, basedir);
+            LineStream capture = new LineStream();
+            try { 
+                lineate(capture, handler, basedir, file);
+            } catch (IOException e) {
+                MessageUtil.fail(handler, 
+                    "NormalizedCompareFiles IOException reading " + file, e);
+                return null;
+            } finally {
+                if (null != in) {
+                    try { in.close(); }
+                    catch (IOException e) {} // ignore
+                }
+                capture.flush();
+                capture.close();
+            }
+            String missed = capture.getMissed();
+            if (!LangUtil.isEmpty(missed)) {
+                MessageUtil.warn(handler, 
+                    "NormalizedCompareFiles missed input: " 
+                    + missed);
+                return null;
+            } else {
+                String[] lines = capture.getLines();
+                CanonicalLine[] result = new CanonicalLine[lines.length];
+                for (int i = 0; i < lines.length; i++) {
+                    result[i] = new CanonicalLine(lines[i], lines[i]);
+                }
+                return result;
+            }                    
+        } 
+        
+        protected abstract void lineate(
+            PrintStream sink,
+            IMessageHandler handler, 
+            File basedir, 
+            File file) throws IOException;
+    }
+    
+    private static class TextLineator extends Lineator {
+        
+        protected void lineate(
+            PrintStream sink,
+            IMessageHandler handler, 
+            File basedir, 
+            File file) throws IOException {
+            InputStream in = null;
+            try {
+                in = new FileInputStream(file);
+                FileUtil.copyStream(new DataInputStream(in), sink);
+            } finally {
+                try { in.close(); }
+                catch (IOException e) {} // ignore
+            }
+        }
+    }
+
+    public static class ClassLineator extends Lineator {
+        
+        protected void lineate(
+            PrintStream sink,
+            IMessageHandler handler, 
+            File basedir, 
+            File file) throws IOException {
+            String name = FileUtil.fileToClassName(basedir, file);
+            // XXX re-enable preflight?
+//            if ((null != basedir) && (path.length()-6 != name.length())) {
+//                MessageUtil.error(handler, "unexpected class name \""
+//                        + name + "\" for path " + path);
+//                return null;
+//            }
+            disassemble(handler, basedir, name, sink);
+        }
+        
+        public static boolean haveDisassembler() {
+            try {
+                return (null != Class.forName("org.aspectj.weaver.bcel.LazyClassGen"));
+            } catch (ClassNotFoundException e) {
+                // XXX fix
+                //System.err.println(e.getMessage());
+                //e.printStackTrace(System.err);
+                return false;
+            }
+        }
+        
+        /** XXX  dependency on bcweaver/bcel */
+        private static void disassemble(
+            IMessageHandler handler, 
+            File basedir, 
+            String name, 
+            PrintStream out) throws IOException {
+            // LazyClassGen.disassemble(FileUtil.normalizedPath(basedir), name, capture);
+            
+            Throwable thrown = null;
+            String basedirPath = FileUtil.normalizedPath(basedir);
+            // XXX use reflection utilities to invoke dissassembler?
+            try {
+                // XXX need test to detect when this is refactored
+                Class c = Class.forName("org.aspectj.weaver.bcel.LazyClassGen");
+                Method m = c.getMethod("disassemble", 
+                    new Class[] {String.class, String.class, PrintStream.class});
+                m.invoke(null, new Object[] { basedirPath, name, out});
+            } catch (ClassNotFoundException e) {
+                thrown = e;
+            } catch (NoSuchMethodException e) {
+                thrown = e;
+            } catch (IllegalAccessException e) {
+                thrown = e;
+            } catch (InvocationTargetException e) {
+                Throwable t = e.getTargetException();
+                if (t instanceof IOException) {
+                    throw (IOException) t;
+                }
+                thrown = t;
+            }
+            if (null != thrown) {
+                MessageUtil.fail(handler, "disassembling " + name  + " path: " + basedirPath,
+                    thrown);
+            }
+        }
+    }
+
+
+    /**
+     * Capture PrintStream output to String[]
+     * (delimiting component String on println()),
+     * also showing any missed text.
+     */
+    public static class LineStream extends PrintStream {
+        StringBuffer sb = new StringBuffer();
+        ByteArrayOutputStream missed;
+        ArrayList sink;
+        public LineStream() {
+            super(new ByteArrayOutputStream());
+            this.sink = new ArrayList();
+            missed = (ByteArrayOutputStream) out;
+        }
+        
+        /** @return any text not captured by our overrides */
+        public String getMissed() {
+            return missed.toString();
+        }
+        
+        /** clear captured lines (but not missed text) */
+        public void clear() {
+            sink.clear();
+        }
+        
+        /** 
+         * Get String[] of lines printed,
+         * delimited by println(..) calls.
+         * @return lines printed, exclusive of any not yet terminated by newline 
+         */
+        public String[] getLines() {
+            return (String[]) sink.toArray(new String[0]);
+        }
+        
+        // ---------- PrintStream overrides
+               public void println(Object x) {
+                       println(x.toString());
+               }
+
+               public void print(Object obj) {
+                       print(obj.toString());
+               }
+
+        public void println(char c) {
+            sb.append(c);
+            println();
+        }
+        public void println(char[] c) {
+            sb.append(c);
+            println();
+        }
+               public void print(char c) {
+            sb.append(c);
+               }
+
+        public void print(char[] c) {
+            sb.append(c);
+        }
+        
+        public void println(String s) {
+            print(s);
+            println();
+        }
+        public void print(String s) {
+            sb.append(s);
+        }
+        public void println() {
+            String line = sb.toString();
+            sink.add(line);
+            sb.setLength(0);
+        }
+    }
+    
+}
diff --git a/testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile$1.class b/testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile$1.class
new file mode 100644 (file)
index 0000000..c5db4e9
Binary files /dev/null and b/testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile$1.class differ
diff --git a/testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile.class b/testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile.class
new file mode 100644 (file)
index 0000000..52c60e6
Binary files /dev/null and b/testing-util/testdata/testCompareClassFiles/org/aspectj/testing/util/TestCompareClassFile.class differ
diff --git a/testing-util/testdata/testCompareTextFiles/differentFile/actual/TestUtilTest.java b/testing-util/testdata/testCompareTextFiles/differentFile/actual/TestUtilTest.java
new file mode 100644 (file)
index 0000000..bb8d929
--- /dev/null
@@ -0,0 +1,95 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This file is part of the compiler and core tools for the AspectJ(tm)
+ * programming language; see http://aspectj.org
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * either http://www.mozilla.org/MPL/ or http://aspectj.org/MPL/.
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is AspectJ.
+ *
+ * The Initial Developer of the Original Code is Xerox Corporation. Portions
+ * created by Xerox Corporation are Copyright (C) 1999-2002 Xerox Corporation.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ */
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TestUtilTest extends TestCase {
+
+       public TestUtilTest(String name) {
+               super(name);
+       }
+    
+    public void testFileCompareNonClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File thisFile = new File("testsrc/org/aspectj/testing/util/TestUtilTest.java");
+        //File thisFile = new File("src/testing-util.lst");
+        assertTrue(TestUtil.sameFiles(holder, thisFile, thisFile));
+        
+        File tempFile = File.createTempFile("TestUtilTest", ".tmp");
+        FileUtil.copyFile(thisFile, tempFile);
+        long len = tempFile.length();
+        assertTrue(0 != len);
+        long tlen = thisFile.length();
+        assertEquals(tlen, len);
+        assertTrue(TestUtil.sameFiles(holder, tempFile, thisFile));
+        try {
+            String path = thisFile.getName();
+            File basedir = tempFile.getParentFile();        
+            File renamed = new File(basedir, path);
+            if (!tempFile.renameTo(renamed)) {
+                MessageUtil.warn(holder, "unable to rename " + tempFile + " to " + renamed);
+            } else {
+                len = renamed.length();
+                assertEquals(tlen, len);
+                assertTrue(TestUtil.sameFiles(holder, basedir, thisFile.getParentFile(), path));
+            }
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+                holder.clear();
+            }
+            tempFile.delete();
+        }
+    }
+
+    /** added line here */
+    public void testFileCompareClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File classBase = new File("testdata/testCompareClassFiles");
+        String path = "org/aspectj/testing/util/TestCompareClassFile.class";
+        File classFile = new File(classBase, path);
+        
+        try {
+            assertTrue(TestUtil.sameFiles(holder, classFile, classFile));
+            assertTrue(TestUtil.sameFiles(holder, classBase, classBase, path));
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+            }
+        }
+    }
+    
+}
diff --git a/testing-util/testdata/testCompareTextFiles/differentFile/expected/TestUtilTest.java b/testing-util/testdata/testCompareTextFiles/differentFile/expected/TestUtilTest.java
new file mode 100644 (file)
index 0000000..fb87e8a
--- /dev/null
@@ -0,0 +1,94 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This file is part of the compiler and core tools for the AspectJ(tm)
+ * programming language; see http://aspectj.org
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * either http://www.mozilla.org/MPL/ or http://aspectj.org/MPL/.
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is AspectJ.
+ *
+ * The Initial Developer of the Original Code is Xerox Corporation. Portions
+ * created by Xerox Corporation are Copyright (C) 1999-2002 Xerox Corporation.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ */
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TestUtilTest extends TestCase {
+
+       public TestUtilTest(String name) {
+               super(name);
+       }
+    
+    public void testFileCompareNonClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File thisFile = new File("testsrc/org/aspectj/testing/util/TestUtilTest.java");
+        //File thisFile = new File("src/testing-util.lst");
+        assertTrue(TestUtil.sameFiles(holder, thisFile, thisFile));
+        
+        File tempFile = File.createTempFile("TestUtilTest", ".tmp");
+        FileUtil.copyFile(thisFile, tempFile);
+        long len = tempFile.length();
+        assertTrue(0 != len);
+        long tlen = thisFile.length();
+        assertEquals(tlen, len);
+        assertTrue(TestUtil.sameFiles(holder, tempFile, thisFile));
+        try {
+            String path = thisFile.getName();
+            File basedir = tempFile.getParentFile();        
+            File renamed = new File(basedir, path);
+            if (!tempFile.renameTo(renamed)) {
+                MessageUtil.warn(holder, "unable to rename " + tempFile + " to " + renamed);
+            } else {
+                len = renamed.length();
+                assertEquals(tlen, len);
+                assertTrue(TestUtil.sameFiles(holder, basedir, thisFile.getParentFile(), path));
+            }
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+                holder.clear();
+            }
+            tempFile.delete();
+        }
+    }
+
+    public void testFileCompareClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File classBase = new File("testdata/testCompareClassFiles");
+        String path = "org/aspectj/testing/util/TestCompareClassFile.class";
+        File classFile = new File(classBase, path);
+        
+        try {
+            assertTrue(TestUtil.sameFiles(holder, classFile, classFile));
+            assertTrue(TestUtil.sameFiles(holder, classBase, classBase, path));
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+            }
+        }
+    }
+    
+}
diff --git a/testing-util/testdata/testCompareTextFiles/sameFile/actual/TestUtilTest.java b/testing-util/testdata/testCompareTextFiles/sameFile/actual/TestUtilTest.java
new file mode 100644 (file)
index 0000000..fb87e8a
--- /dev/null
@@ -0,0 +1,94 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This file is part of the compiler and core tools for the AspectJ(tm)
+ * programming language; see http://aspectj.org
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * either http://www.mozilla.org/MPL/ or http://aspectj.org/MPL/.
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is AspectJ.
+ *
+ * The Initial Developer of the Original Code is Xerox Corporation. Portions
+ * created by Xerox Corporation are Copyright (C) 1999-2002 Xerox Corporation.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ */
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TestUtilTest extends TestCase {
+
+       public TestUtilTest(String name) {
+               super(name);
+       }
+    
+    public void testFileCompareNonClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File thisFile = new File("testsrc/org/aspectj/testing/util/TestUtilTest.java");
+        //File thisFile = new File("src/testing-util.lst");
+        assertTrue(TestUtil.sameFiles(holder, thisFile, thisFile));
+        
+        File tempFile = File.createTempFile("TestUtilTest", ".tmp");
+        FileUtil.copyFile(thisFile, tempFile);
+        long len = tempFile.length();
+        assertTrue(0 != len);
+        long tlen = thisFile.length();
+        assertEquals(tlen, len);
+        assertTrue(TestUtil.sameFiles(holder, tempFile, thisFile));
+        try {
+            String path = thisFile.getName();
+            File basedir = tempFile.getParentFile();        
+            File renamed = new File(basedir, path);
+            if (!tempFile.renameTo(renamed)) {
+                MessageUtil.warn(holder, "unable to rename " + tempFile + " to " + renamed);
+            } else {
+                len = renamed.length();
+                assertEquals(tlen, len);
+                assertTrue(TestUtil.sameFiles(holder, basedir, thisFile.getParentFile(), path));
+            }
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+                holder.clear();
+            }
+            tempFile.delete();
+        }
+    }
+
+    public void testFileCompareClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File classBase = new File("testdata/testCompareClassFiles");
+        String path = "org/aspectj/testing/util/TestCompareClassFile.class";
+        File classFile = new File(classBase, path);
+        
+        try {
+            assertTrue(TestUtil.sameFiles(holder, classFile, classFile));
+            assertTrue(TestUtil.sameFiles(holder, classBase, classBase, path));
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+            }
+        }
+    }
+    
+}
diff --git a/testing-util/testdata/testCompareTextFiles/sameFile/expected/TestUtilTest.java b/testing-util/testdata/testCompareTextFiles/sameFile/expected/TestUtilTest.java
new file mode 100644 (file)
index 0000000..fb87e8a
--- /dev/null
@@ -0,0 +1,94 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This file is part of the compiler and core tools for the AspectJ(tm)
+ * programming language; see http://aspectj.org
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * either http://www.mozilla.org/MPL/ or http://aspectj.org/MPL/.
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is AspectJ.
+ *
+ * The Initial Developer of the Original Code is Xerox Corporation. Portions
+ * created by Xerox Corporation are Copyright (C) 1999-2002 Xerox Corporation.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ */
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TestUtilTest extends TestCase {
+
+       public TestUtilTest(String name) {
+               super(name);
+       }
+    
+    public void testFileCompareNonClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File thisFile = new File("testsrc/org/aspectj/testing/util/TestUtilTest.java");
+        //File thisFile = new File("src/testing-util.lst");
+        assertTrue(TestUtil.sameFiles(holder, thisFile, thisFile));
+        
+        File tempFile = File.createTempFile("TestUtilTest", ".tmp");
+        FileUtil.copyFile(thisFile, tempFile);
+        long len = tempFile.length();
+        assertTrue(0 != len);
+        long tlen = thisFile.length();
+        assertEquals(tlen, len);
+        assertTrue(TestUtil.sameFiles(holder, tempFile, thisFile));
+        try {
+            String path = thisFile.getName();
+            File basedir = tempFile.getParentFile();        
+            File renamed = new File(basedir, path);
+            if (!tempFile.renameTo(renamed)) {
+                MessageUtil.warn(holder, "unable to rename " + tempFile + " to " + renamed);
+            } else {
+                len = renamed.length();
+                assertEquals(tlen, len);
+                assertTrue(TestUtil.sameFiles(holder, basedir, thisFile.getParentFile(), path));
+            }
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+                holder.clear();
+            }
+            tempFile.delete();
+        }
+    }
+
+    public void testFileCompareClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File classBase = new File("testdata/testCompareClassFiles");
+        String path = "org/aspectj/testing/util/TestCompareClassFile.class";
+        File classFile = new File(classBase, path);
+        
+        try {
+            assertTrue(TestUtil.sameFiles(holder, classFile, classFile));
+            assertTrue(TestUtil.sameFiles(holder, classBase, classBase, path));
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+            }
+        }
+    }
+    
+}
diff --git a/testing-util/testsrc/TestingUtilModuleTests.java b/testing-util/testsrc/TestingUtilModuleTests.java
new file mode 100644 (file)
index 0000000..32084c1
--- /dev/null
@@ -0,0 +1,32 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+// default package
+
+import org.aspectj.testing.util.UtilTests;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+public class TestingUtilModuleTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(TestingUtilModuleTests.class.getName());
+        suite.addTest(UtilTests.suite()); 
+        return suite;
+    }
+
+    public TestingUtilModuleTests(String name) { super(name); }
+}  
diff --git a/testing-util/testsrc/org/aspectj/testing/util/TestCompareClassFile.java b/testing-util/testsrc/org/aspectj/testing/util/TestCompareClassFile.java
new file mode 100644 (file)
index 0000000..e4dcf80
--- /dev/null
@@ -0,0 +1,148 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.util.LangUtil;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+/**
+ * This is source for a sample .class file.
+ * It is compiled and the corresponding .class files are
+ * checked in under the testdata directory.
+ * It has no other purpose.
+ */
+public class TestCompareClassFile implements Runnable {
+    public static final String STATIC_CONST = "STATIC_CONST";
+       public static void main(String[] args) {
+        // tc static references
+        long l = Math.abs(System.currentTimeMillis());
+          String s = STATIC_CONST + " is constant";
+    }
+    public static void runStatic() {
+    }
+    private static void privateRunStatic() {
+    }
+    static void defaultRunStatic() {
+    }
+    protected static void protectedRunStatic() {
+    }
+    
+    private long privateLong;
+    private final Object privateFinalObject;
+    
+    private TestCompareClassFile() {
+        super();
+        privateLong = System.currentTimeMillis();
+        // method-local inner class
+        privateFinalObject = new Runnable() { public void run(){}};
+    }
+
+    /** implement Runnable */
+    public void run() {
+    }
+    private void privateRun() {
+    }
+    void defaultRun() {
+    }
+    protected void protectedRun() {
+    }
+    
+    // ------- misc stolen utility code
+    // Collections Util
+    public static List getListInMap(Map map, Object key) {
+        List list = (List)map.get(key);
+        if (list == null) {
+            list = new ArrayList();
+            map.put(key, list);
+        }
+        return list;
+    }
+
+    public static SortedSet getSortedSetInMap(Map map, Object key) {
+        SortedSet list = (SortedSet)map.get(key);
+        if (list == null) {
+            list = new TreeSet();
+            map.put(key, list);
+        }
+        return list;
+    }
+    
+    // LangUtil
+    /**
+     * Make a copy of the array.
+     * @return an array with the same component type as source
+     * containing same elements, even if null.
+     * @throws IllegalArgumentException if source is null
+     */
+    public static final Object[] copy(Object[] source) {
+        final Class c = source.getClass().getComponentType();
+        Object[] result = (Object[]) Array.newInstance(c, source.length);
+        System.arraycopy(source, 0, result, 0, result.length);
+        return result;
+    }
+    /**
+     * Trim ending lines from a StringBuffer,
+     * clipping to maxLines and further removing any number of
+     * trailing lines accepted by checker.
+     * @param checker returns true if trailing line should be elided.
+     * @param stack StringBuffer with lines to elide
+     * @param maxLines int for maximum number of resulting lines
+     */
+    static void elideEndingLines(StringBuffer stack, int maxLines) {
+        if ((null == stack) || (0 == stack.length())) {
+            return;
+        }
+        final LinkedList lines = new LinkedList();
+        StringTokenizer st = new StringTokenizer(stack.toString(),"\n\r");
+        while (st.hasMoreTokens() && (0 < --maxLines)) {
+            lines.add(st.nextToken());
+        }
+        st = null;
+        
+        String line;
+        int elided = 0;
+        while (!lines.isEmpty()) { 
+            line = (String) lines.getLast();
+            if (null == line) {
+                break;
+            } else {
+                elided++;
+                lines.removeLast();
+            }
+        }
+        if ((elided > 0) || (maxLines < 1)) { 
+            final int EOL_LEN = LangUtil.EOL.length();           
+            int totalLength = 0;
+            while (!lines.isEmpty()) { 
+                totalLength += EOL_LEN + ((String) lines.getFirst()).length();
+                lines.removeFirst();
+            }
+            if (stack.length() > totalLength) {
+                stack.setLength(totalLength);
+                if (elided > 0) {
+                    stack.append("    (... " + elided + " lines...)");
+                }
+            }
+        }
+    }
+    
+}
diff --git a/testing-util/testsrc/org/aspectj/testing/util/TestUtilTest.java b/testing-util/testsrc/org/aspectj/testing/util/TestUtilTest.java
new file mode 100644 (file)
index 0000000..73da9cf
--- /dev/null
@@ -0,0 +1,116 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TestUtilTest extends TestCase {
+
+       public TestUtilTest(String name) {
+               super(name);
+       }
+    
+    public void testFileCompareNonClass() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File thisFile = new File("testsrc/org/aspectj/testing/util/TestUtilTest.java");
+        //File thisFile = new File("src/testing-util.lst");
+        assertTrue(TestUtil.sameFiles(holder, thisFile, thisFile));
+        
+        File tempFile = File.createTempFile("TestUtilTest", ".tmp");
+        FileUtil.copyFile(thisFile, tempFile);
+        long len = tempFile.length();
+        assertTrue(0 != len);
+        long tlen = thisFile.length();
+        assertEquals(tlen, len);
+        assertTrue(TestUtil.sameFiles(holder, tempFile, thisFile));
+        try {
+            String path = thisFile.getName();
+            File basedir = tempFile.getParentFile();        
+            File renamed = new File(basedir, path);
+            if (!tempFile.renameTo(renamed)) {
+                MessageUtil.warn(holder, "unable to rename " + tempFile + " to " + renamed);
+            } else {
+                len = renamed.length();
+                assertEquals(tlen, len);
+                assertTrue(TestUtil.sameFiles(holder, basedir, thisFile.getParentFile(), path));
+            }
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+                holder.clearMessages();
+            }
+            tempFile.delete();
+        }
+    }
+
+    public void testFileCompareNonClassStaticPositive() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File basedir = new File("testdata/testCompareTextFiles/sameFile");
+        File expectedBaseDir = new File(basedir, "expected");
+        File actualBaseDir = new File(basedir, "actual");
+        String filename = "TestUtilTest.java";
+        File expected = new File(expectedBaseDir, filename);
+        File actual = new File(actualBaseDir, filename);
+
+        assertTrue(TestUtil.sameFiles(holder, expected, actual));
+
+        assertTrue(TestUtil.sameFiles(holder, expectedBaseDir, actualBaseDir, filename));
+    }
+
+    public void testFileCompareNonClassStaticNegative() throws IOException {
+        MessageHandler holder = new MessageHandler();
+        File basedir = new File("testdata/testCompareTextFiles/differentFile");
+        File expectedBaseDir = new File(basedir, "expected");
+        File actualBaseDir = new File(basedir, "actual");
+        String filename = "TestUtilTest.java";
+        File expected = new File(expectedBaseDir, filename);
+        File actual = new File(actualBaseDir, filename);
+
+        assertTrue(!TestUtil.sameFiles(holder, expected, actual));
+
+        assertTrue(!TestUtil.sameFiles(holder, expectedBaseDir, actualBaseDir, filename));
+    }
+
+    public void testFileCompareClass() throws IOException {
+        if (!TestUtil.ClassLineator.haveDisassembler()) {
+            System.err.println("skipping testFileCompareClass - no disassembler on classpath");
+            return;
+        }
+        MessageHandler holder = new MessageHandler();
+        File classBase = new File("testdata/testCompareClassFiles");
+        String path = "org/aspectj/testing/util/TestCompareClassFile.class";
+        File classFile = new File(classBase, path);
+        
+        try {
+            assertTrue(TestUtil.sameFiles(holder, classFile, classFile));
+            assertTrue(TestUtil.sameFiles(holder, classBase, classBase, path));
+        } finally {
+            if (0 < holder.numMessages(null, true)) {
+                MessageUtil.print(System.out, holder);
+            }
+        }
+    }
+    
+}
diff --git a/testing-util/testsrc/org/aspectj/testing/util/UtilTests.java b/testing-util/testsrc/org/aspectj/testing/util/UtilTests.java
new file mode 100644 (file)
index 0000000..a32bd42
--- /dev/null
@@ -0,0 +1,32 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import junit.framework.*;
+
+public class UtilTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(UtilTests.class.getName());
+        // for now, do not include SuiteTest because it would take 15 minutes
+        //$JUnit-BEGIN$
+        suite.addTestSuite(TestUtilTest.class); 
+        //$JUnit-END$
+        return suite;
+    }
+
+    public UtilTests(String name) { super(name); }
+
+}  
diff --git a/testing/.classpath b/testing/.classpath
new file mode 100644 (file)
index 0000000..f24197a
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+    <classpathentry kind="src" path="src"/>
+    <classpathentry kind="src" path="testsrc"/>
+    <classpathentry kind="var" path="JRE_LIB" rootpath="JRE_SRCROOT" sourcepath="JRE_SRC"/>
+    <classpathentry kind="lib" path="/lib/ant/lib/ant.jar" sourcepath="/lib/ant/ant-src.zip"/>
+    <classpathentry kind="lib" path="/lib/junit/junit.jar" sourcepath="/lib/junit/junit-src.jar"/>
+    <classpathentry kind="lib" path="/lib/jdiff/jdiff.jar"/>
+    <classpathentry kind="lib" path="/lib/regexp/jakarta-regexp-1.2.jar"/>
+    <classpathentry kind="src" path="/bridge"/>
+    <classpathentry kind="src" path="/util"/>
+    <classpathentry kind="src" path="/testing-client"/>
+    <classpathentry kind="lib" path="/lib/ant/lib/xercesImpl.jar"/>
+    <classpathentry kind="lib" path="/lib/ant/lib/xml-apis.jar"/>
+    <classpathentry kind="lib" path="/lib/commons/commons.jar" sourcepath="/lib/commons/commons-src.zip"/>
+    <classpathentry kind="src" path="/testing-util"/>
+    <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/testing/.project b/testing/.project
new file mode 100644 (file)
index 0000000..12bc715
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>testing</name>
+       <comment></comment>
+       <projects>
+               <project>bridge</project>
+               <project>testing-client</project>
+               <project>testing-util</project>
+               <project>util</project>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/testing/src/.cvsignore b/testing/src/.cvsignore
new file mode 100644 (file)
index 0000000..a3f0b1b
--- /dev/null
@@ -0,0 +1 @@
+*.lst
diff --git a/testing/src/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java b/testing/src/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java
new file mode 100644 (file)
index 0000000..ec112d2
--- /dev/null
@@ -0,0 +1,1864 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.internal.tools.ant.taskdefs;
+
+import java.beans.*;
+import java.io.*;
+import java.util.*;
+import java.util.List;
+import java.text.*;
+import org.apache.tools.ant.*;
+import org.apache.tools.ant.taskdefs.*;
+import org.apache.tools.ant.types.*;
+import org.aspectj.util.LangUtil;
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+import javax.swing.text.*;
+
+public class Ajctest extends Task implements PropertyChangeListener {
+    private static Ajctest CURRENT_AJCTEST;
+
+    // todo shutdown hook assumes one task per VM 
+    public Ajctest() {
+        super();
+        CURRENT_AJCTEST = this;
+    }
+
+    private static boolean firstTime = true;
+
+    public final PropertyChangeSupport bean = new PropertyChangeSupport(this);
+
+    {
+        bean.addPropertyChangeListener(this);
+    }
+
+    public void propertyChange(PropertyChangeEvent evt) {
+        String name = evt.getPropertyName();
+        if      ("ajdoc.good".equals(name))  ajdocStats.goods++;
+        else if ("ajc.good".equals(name))    ajcStats.goods++;
+        else if ("run.good".equals(name))    runStats.goods++;
+        if      ("ajdoc.fail".equals(name))  ajdocStats.fails++;
+        else if ("ajc.fail".equals(name))    ajcStats.fails++;
+        else if ("run.fail".equals(name))    runStats.fails++;
+    }
+
+    private void fire(String prop, Object oldval, Object newval) {
+        bean.firePropertyChange(prop, oldval, newval);
+    }
+
+    private void fire(String prop) {
+        fire(prop, "dummy-old", "dummy-new");
+    }
+    
+    private static boolean dumpresults = false;
+    private static Stats ajdocStats = new Stats();
+    private static Stats ajcStats   = new Stats();
+    private static Stats runStats   = new Stats();
+    private static Stats errorStats   = new Stats();
+    private static final String NO_TESTID = "NONE";    
+    private File workingdir = new File("ajworkingdir"); //XXX
+
+    //fields
+    private String testId = NO_TESTID;
+    private List args = new Vector();
+    private List testsets = new Vector();
+    private Path classpath;
+    private Path internalclasspath;
+    private File destdir;
+    private File dir;
+    private File errordir;
+    private File errorfile;
+    private List testclasses = new Vector();
+    private boolean nocompile;
+    private Ajdoc ajdoc = null;
+    private boolean noclean;
+    private boolean noverify;
+    private List depends = new Vector();
+    //end-fields
+
+    public Argfile createArgfile() {
+        return createTestset().createArgfile();
+    }
+      
+    public void setNoverify(boolean input) {
+        if (input != noverify) noverify = input;
+    }
+
+    public void setOwningTarget(Target target) {
+        super.setOwningTarget(target);
+        if (null != target) {
+            //setTestId(target.getName());
+        }
+    }
+    
+    public void setTestId(String str) {
+        if ((null != str) && (0 < str.trim().length())) {
+            testId = str;
+        }
+    }
+    
+    public void setArgs(String str) {
+        if (str == null || str.length() < 1) return;
+        StringTokenizer tok = new StringTokenizer(str, ",", false);
+        while (tok.hasMoreTokens()) {
+            String name = tok.nextToken().trim();
+            if (0 < name.length()) {
+              parse(name.startsWith("J") ? createJarg() : createArg(), name);
+            }
+        }
+    }
+
+    private void parse(Argument arg, String name) {
+        int itilde = name.lastIndexOf("~");
+        if (itilde != -1) {
+            name = name.substring(0, itilde) + name.substring(itilde+1);
+        }
+        int ieq = name.lastIndexOf("=");
+        int icolon  = name.lastIndexOf(":");
+        int ileft = name.lastIndexOf("[");
+        int iright = name.lastIndexOf("]");
+        boolean always = true;
+        String rest = "";
+        String newName = name;
+        if (ieq != -1) {
+            rest = name.substring(ieq+1);
+            newName = name.substring(0, ieq);
+            always = true;
+        } else if (icolon != -1) {
+            rest = name.substring(icolon+1);
+            newName = name.substring(0, icolon);
+            always = false;
+        } else if (ileft != -1) {
+            newName = name.substring(0, ileft);
+            always = true;
+        }
+        String values =  ileft == -1 ? rest :
+            name.substring(ileft+1, iright > ileft ? iright : rest.length()-1);
+        String value = null;
+        if (itilde != -1) {
+            String prop = project.getUserProperty(values);
+            if (prop == null) {
+                prop = project.getProperty(values);
+            }
+            if (prop != null) {
+                value = prop;
+            }
+        }
+        if (value != null) {
+            arg.setValue(value);
+        } else {
+            arg.setValues(values);
+        }
+        arg.setName(newName);
+        arg.setAlways(always);
+    }
+
+    public Argument createJarg() {
+        Argument arg = new Argument(true);
+        args.add(arg);
+        return arg;
+    }
+
+    
+    public Argument createArg() {
+        Argument arg = new Argument(false);
+        args.add(arg);
+        return arg;
+    }
+
+    public void setClasspath(Path path) {
+        if (classpath == null) {
+            classpath = path;
+        } else {
+            classpath.append(path);
+        }
+    }
+
+    public Path createClasspath() {
+        if (classpath == null) {
+            classpath = new Path(project);
+        }
+        return classpath.createPath();
+    }
+
+    public void setClasspathRef(Reference r) {
+        createClasspath().setRefid(r);
+    }
+
+    public void setInternalclasspath(Path path) {
+        if (internalclasspath == null) {
+            internalclasspath = path;
+        } else {
+            internalclasspath.append(path);
+        }
+    }
+
+    public Path createInternalclasspath() {
+        if (internalclasspath == null) {
+            internalclasspath = new Path(project);
+        }
+        return internalclasspath.createPath();
+    }
+
+    public void setInternalclasspathRef(Reference r) {
+        createInternalclasspath().setRefid(r);
+    }
+
+    public void setDestdir(String destdir) {
+        this.destdir = project.resolveFile(destdir);
+    }
+
+    public void setDir(File dir) {
+        this.dir = dir;
+    }
+
+    public void setErrordir(File dir) {
+        this.errordir = errordir;
+    }
+
+    public void setErrorfile(File errorfile) {
+        this.errorfile = errorfile;
+    }
+
+    public Run createJava() {
+        Run testclass = new Run(project);
+        testclasses.add(testclass);
+        return testclass;
+    }
+
+    public void setClasses(String str) {
+        for (StringTokenizer t = new StringTokenizer(str, ", ", false);
+             t.hasMoreTokens();) {
+            createJava().setClassname(t.nextToken().trim());
+        }
+    }
+
+    public void setTestclass(String testclass) {
+        createJava().setClassname(testclass);
+    }
+
+    public void setAjdoc(boolean b) {
+        if (b && ajdoc == null) {
+            createAjdoc();
+        } else if (!b) {
+            ajdoc = null;
+        }
+    }
+
+    public void setAjdocargs(String str) {
+        createAjdoc();
+        for (StringTokenizer t = new StringTokenizer(str, ", ", false);
+             t.hasMoreTokens();) {
+            ajdoc.createArg().setValue(t.nextToken().trim());
+        }
+    }
+    
+    public void addAjdoc(Ajdoc ajdoc) {
+        this.ajdoc = ajdoc;
+    }
+    
+    public Ajdoc createAjdoc() {
+        return ajdoc = new Ajdoc();
+    }
+
+    public static class Argument {
+        private String name;
+        private List values = new Vector();
+        private boolean always = true;
+        final boolean isj;
+        public Argument(boolean isj) {
+            this.isj = isj;
+        }
+        public void setName(String str) {
+            this.name = str.startsWith("-") ? str :
+                ("-" + (str.startsWith("J") ? str.substring(1) : str));
+        }
+        public void setValues(String str) {
+            values = new Vector();
+            StringTokenizer tok = new StringTokenizer(str, ", ", false);
+            while (tok.hasMoreTokens()) {
+                values.add(tok.nextToken().trim());
+            }
+        }
+        public void setValue(String value) {
+            (values = new Vector()).add(value);
+        }
+        public void setAlways(boolean always) {
+            this.always = always;
+        }
+        public String toString() { return name + ":" + values; }
+    }
+
+    public void setNocompile(boolean nocompile) {
+        this.nocompile = nocompile;
+    }
+
+    private static class Stats {
+        int goods = 0;
+        int fails = 0;
+    }
+
+    private static class Arg {
+        final String name;
+        final String value;
+        final boolean isj;
+        Arg(String name, String value, boolean isj) {
+            this.name = name;
+            this.value = value;
+            this.isj = isj;
+        }
+        public String toString() {
+            return name + (!"".equals(value) ? " " + value : "");
+        }
+    }
+
+    public Testset createTestset() {
+        Testset testset = new Testset();
+        testsets.add(testset);
+        return testset;
+    }
+
+    public void setNoclean(boolean noclean) {
+        this.noclean = noclean;
+    }
+
+    public void setDepends(String depends) {
+        for (StringTokenizer t = new StringTokenizer(depends, ", ", false);
+             t.hasMoreTokens();) {
+            this.depends.add(t.nextToken().trim());
+        }
+    }
+    //end-methods
+    
+    public static class Argfile {
+        private String name;
+        public void setName(String name) { this.name = name; }
+    }
+
+    public class Ajdoc {
+        private Commandline cmd = new Commandline();
+        public Commandline.Argument createArg() { return cmd.createArgument(); }
+        public Commandline getCommandline() { return cmd; }
+        public String toString() { return cmd + ""; }
+    }
+
+    public class Testset extends FileSet {
+        private List argfileNames = new Vector();
+        public List argfiles;
+        public List files;
+        public List args = new Vector();
+        public String classname;
+        private boolean havecludes = false;
+        private List testclasses = new Vector();
+        private Path classpath;
+        private Path internalclasspath;
+        private Ajdoc ajdoc = null;
+        private boolean fork = false;
+        private boolean noclean;
+        private List depends = new Vector();
+        public String toString() {
+            String str = "";
+            if (files.size() > 0) {
+                str += "files:" + "\n";
+                for (Iterator i = files.iterator(); i.hasNext();) {
+                    str += "\t" + i.next() + "\n";
+                }
+            }
+            if (argfiles.size() > 0) {
+                str += "argfiles:" + "\n";
+                for (Iterator i = argfiles.iterator(); i.hasNext();) {
+                    str += "\t" + i.next() + "\n";
+                }
+            }
+            if (args.size() > 0) {
+                str += "args:" + "\n";
+                for (Iterator i = args.iterator(); i.hasNext();) {
+                    str += "\t" + i.next() + "\n";
+                }
+            }
+            if (testclasses.size() > 0) {
+                str += "classes:" + "\n";
+                for (Iterator i = testclasses.iterator(); i.hasNext();) {
+                    str += "\t" + i.next() + "\n";
+                }
+            }
+            return str;
+        }
+        public void setIncludes(String includes) {
+            super.setIncludes(includes);
+            havecludes = true;
+        }
+        public void setExcludes(String excludes) {
+            super.setExcludes(excludes);
+            havecludes = true;
+        }
+        public void setIncludesfile(File includesfile) {
+            super.setIncludesfile(includesfile);
+            havecludes = true;
+        }
+        public void setExcludesfile(File excludesfile) {
+            super.setExcludesfile(excludesfile);
+            havecludes = true;
+        }        
+        
+        public void setArgfile(String name) {
+            createArgfile().setName(name);
+        }
+        
+        public void setArgfiles(String str) {
+            StringTokenizer tok = new StringTokenizer(str, ", ", false);
+            while (tok.hasMoreTokens()) {
+                createArgfile().setName(tok.nextToken().trim());
+            }
+            
+        }
+        public Argfile createArgfile() {
+            Argfile argfile = new Argfile();
+            argfileNames.add(argfile);
+            return argfile;
+        }
+        public Run createJava() {
+            // See crashing note
+            //Run testclass = new Run();
+            Run testclass = new Run(project);
+            this.testclasses.add(testclass);
+            return testclass;
+        }
+        public void addJava(Run run) {
+            this.testclasses.add(run);
+        }
+        public void setJava(String str) {
+            StringTokenizer t = new StringTokenizer(str, " ");
+            Run run = createJava();
+            run.setClassname(t.nextToken().trim());
+            while (t.hasMoreTokens()) {
+                run.createArg().setValue(t.nextToken().trim());
+            }
+        }
+        public void setTestclass(String testclass) {
+            createJava().setClassname(testclass);
+        }
+        
+        public void setClasses(String str) {
+            for (StringTokenizer t = new StringTokenizer(str, ", ", false);
+                 t.hasMoreTokens();) {
+                createJava().setClassname(t.nextToken().trim());
+            }
+        }
+        public void setClasspath(Path path) {
+            if (classpath == null) {
+                classpath = path;
+            } else {
+                classpath.append(path);
+            }
+        }
+        
+        public Path createClasspath() {
+            if (classpath == null) {
+                classpath = new Path(project);
+            }
+            return classpath.createPath();
+        }
+        
+        public void setClasspathRef(Reference r) {
+            createClasspath().setRefid(r);
+        }
+        public void setInternalclasspath(Path path) {
+            if (internalclasspath == null) {
+                internalclasspath = path;
+            } else {
+                internalclasspath.append(path);
+            }
+        }
+        
+        public Path createInternalclasspath() {
+            if (internalclasspath == null) {
+            internalclasspath = new Path(project);
+            }
+            return internalclasspath.createPath();
+        }
+        
+        public void setInternalclasspathRef(Reference r) {
+            createInternalclasspath().setRefid(r);
+        }
+        
+        public void setAjdoc(boolean b) {
+            if (b && ajdoc == null) {
+                createAjdoc();
+            } else if (!b) {
+                this.ajdoc = null;
+            }
+        }
+        public Ajdoc getAjdoc() { return this.ajdoc; }
+        public void setAjdocargs(String str) {
+            createAjdoc();
+            for (StringTokenizer t = new StringTokenizer(str, ", ", false);
+                     t.hasMoreTokens();) {
+                this.ajdoc.createArg().setValue(t.nextToken().trim());
+            }
+        }
+        public void addAjdoc(Ajdoc ajdoc) {
+            this.ajdoc = ajdoc;
+        }
+        public Ajdoc createAjdoc() {
+            return this.ajdoc = new Ajdoc();
+        }
+        public void setFork(boolean fork) {
+            this.fork = fork;
+        }
+        public void setNoclean(boolean noclean) {
+            this.noclean = noclean;
+        }
+        public void setDepends(String depends) {
+            for (StringTokenizer t = new StringTokenizer(depends, ", ", false);
+                 t.hasMoreTokens();) {
+                this.depends.add(t.nextToken().trim());
+            }
+        }
+        //end-testset-methods
+        
+        public void resolve() throws BuildException {
+            if (dir != null) this.setDir(dir);
+            File src = getDir(project);
+            argfiles = new Vector();
+            files = new Vector();
+            for(Iterator iter = argfileNames.iterator(); iter.hasNext();) {
+                String name = ((Argfile)iter.next()).name;
+                File argfile = new File(src, name);
+                if (check(argfile, name, location)) argfiles.add(argfile);
+            }
+            if (havecludes || argfiles.size() <= 0) {
+                String[] filenames =
+                    getDirectoryScanner(project).getIncludedFiles();
+                for (int j = 0; j < filenames.length; j++) {
+                    String name = filenames[j];                    
+                    if (name.endsWith(".java")) {
+                        File file = new File(src, name);
+                        if (check(file, name, location)) files.add(file);
+                    }
+                }
+            }
+            for (Iterator i = Ajctest.this.testclasses.iterator();
+                 i.hasNext();) {
+                this.testclasses.add((Run)i.next());
+            }
+            if (this.classpath == null) {
+                setClasspath(Ajctest.this.classpath);
+            }
+            if (this.internalclasspath == null) {
+                setInternalclasspath(Ajctest.this.internalclasspath);
+            }
+            if (this.ajdoc == null) {
+                this.ajdoc = Ajctest.this.ajdoc;
+            }
+            if (this.fork) {
+                for (Iterator i = this.testclasses.iterator(); i.hasNext();) {
+                    ((Run)i.next()).setFork(fork);
+                }
+            }
+            if (!this.noclean) {
+                this.noclean = Ajctest.this.noclean;
+            }
+            this.depends.addAll(Ajctest.this.depends);
+        }
+        private boolean check(File file, String name, Location loc)
+            throws BuildException {
+            loc = loc != null ? loc : location;
+            if (file == null) {
+                throw new BuildException
+                    ("file " + name + " is null!", loc);
+            }
+            if (!file.exists()) {
+                throw new BuildException
+                    ("file " + file + " with name " + name +
+                     " doesn't exist!", loc);
+            }
+            return true;
+        }
+        public void setArgs(String str) {
+            if (str == null || str.length() < 1) return;
+            StringTokenizer tok = new StringTokenizer(str, ",", false);
+            while (tok.hasMoreTokens()) {
+                String name = tok.nextToken().trim();
+                parse(name.startsWith("J") ? createJarg() : createArg(), name);                
+            }
+        }
+        
+        public Argument createJarg() {
+            Argument arg = new Argument(true);
+            args.add(arg);
+            return arg;
+        }
+        
+        public Argument createArg() {
+            Argument arg = new Argument(false);
+            args.add(arg);
+            return arg;
+        }        
+    }
+
+    private void prepare() throws BuildException {
+        
+    }
+
+    private void finish() throws BuildException {
+        if (errors.size() > 0) {
+            log("");
+            log("There " + w(errors) + " " + errors.size() + " errors:");
+            for (int i = 0; i < errors.size(); i++) {
+                log(" ", (Failure)errors.get(i), i);
+            }
+        }
+        allErrors.addAll(errors);
+    }
+
+    private void log(String space, Failure failure, int num) {
+        String number = "[" + num + "] ";
+        log(enough(number, 60, '-'));
+        for (int i = number.length()-1; i > 0; i--) space += " ";
+        log(space, failure.testset.files, "files:");
+        log(space, failure.testset.argfiles, "argfiles:");
+        log(space, failure.args, "args:");
+        log(space + "msgs:" + failure.msgs);
+    }
+
+    
+    private String enough(String str, int size, char filler) {
+        while (str.length() < size) str += filler;
+        return str;
+    }
+
+    
+    private void log(String space, List list, String title) {
+        if (list == null || list.size() < 1) return;
+        log(space + title);
+        for (Iterator i = list.iterator(); i.hasNext();) {
+            log(space + "  " + i.next());
+        }
+    }
+
+    private void execute(Testset testset, List args) throws BuildException {
+        if (testset.files.size() > 0) {
+            log("\tfiles:");
+            for (Iterator i = testset.files.iterator();
+                 i.hasNext();) {
+                log("\t  " + i.next());
+            }
+        }
+        if (testset.argfiles.size() > 0) {
+            log("\targfiles:");
+            for (Iterator i = testset.argfiles.iterator();
+                 i.hasNext();) {
+                log("\t  " + i.next());
+            }
+        }
+        if (args.size() > 0) {
+            log("\targs:");
+            for (Iterator i = args.iterator();
+                 i.hasNext();) {
+                log("\t  " + i.next());
+            }
+        }
+        if (testset.testclasses.size() > 0) {
+            log("\tclasses:");
+            for (Iterator i = testset.testclasses.iterator();
+                 i.hasNext();) {
+                log("\t  " + i.next());
+            }
+        }
+        if (!testset.noclean &&
+            (!isSet("noclean") && !isSet("nocompile"))) {
+            delete(destdir);
+            make(destdir);
+        }
+        delete(workingdir);
+        make(workingdir);
+        for (Iterator i = testset.depends.iterator(); i.hasNext();) {
+            String target = i.next()+"";
+            // todo: capture failures here?
+            project.executeTarget(target);
+        }
+        int exit;
+        if (!isSet("nodoc") && testset.ajdoc != null) {
+            log("\tdoc... " + testset.ajdoc);
+            AjdocWrapper ajdoc = new AjdocWrapper(testset, args);
+            if ((exit = ajdoc.run()) != 0) {
+                post(testset, args, ajdoc.msgs, exit, "ajdoc");
+            } else {
+                fire("ajdoc.good");
+            }
+            fire("ajdoc.done");
+            log("\tdone with ajdoc.");
+        }
+        boolean goodCompile = true;
+        if (!isSet("nocompile") && !nocompile) {
+            log("\tcompile" +
+                (testset.noclean ? "(boostrapped)" : "") + "...");
+            //AjcWrapper ajc = new AjcWrapper(testset, args);
+            JavaCommandWrapper ajc;
+            // XXX dependency on Ant property ajctest.compiler
+            final String compiler = getAntProperty("ajctest.compiler");
+            if ("eclipse".equals(compiler) || "eajc".equals(compiler)) {
+                ajc = new EAjcWrapper(testset, args);
+            } else if ((null == compiler) || "ajc".equals(compiler)) {
+                ajc = new AjcWrapper(testset, args);
+            } else if ("javac".equals(compiler)) {
+                throw new Error("javac not supported");
+                //ajc = new JavacWrapper(testset, args);
+            } else {
+                throw new Error("unknown compiler: " + compiler);
+            }
+            
+            System.out.println("using compiler: " + ajc);
+            try {            
+                if ((exit = ajc.run()) != 0) {
+                    post(testset, args, ajc.msgs, exit, "ajc");
+                    goodCompile = false;
+                } else {
+                    fire("ajc.good");
+                }
+                fire("ajc.done");
+            } catch (Throwable ___) {
+                post(testset, args, ___+"", -1, "ajc");
+                goodCompile = false;
+            }
+        }
+        if (!goodCompile) {
+            post(testset, new Vector(),
+                 "couldn't run classes " + testset.testclasses +
+                 "due to failed compile",
+                 -1, "run");
+            
+        } else if (!isSet("norun")) {
+            for (Iterator i = testset.testclasses.iterator();
+                 i.hasNext();) {
+                Run testclass = (Run)i.next();
+                log("\ttest..." + testclass.classname());
+                if (null != destdir) {
+                    testclass.setClassesDir(destdir.getAbsolutePath());
+                }
+                if ((exit = testclass.executeJava()) != 0) {
+                    post(testset, new Vector(), testclass.msgs, exit, "run");
+                } else {
+                    fire("run.good");
+                }
+                fire("run.done");
+            }
+        }
+        log("");
+    }
+
+    public void execute() throws BuildException {
+        gui(this);
+        dumpresults = isSet("dumpresults");
+        prepare();
+        log(testsets.size() + " testset" + s(testsets),
+            Project.MSG_VERBOSE);
+        Map testsetToArgcombo = new HashMap();
+        List argcombos = new Vector();
+        for (Iterator iter = testsets.iterator(); iter.hasNext();) {
+            Testset testset = (Testset)iter.next();
+            testset.resolve();
+            List bothargs = new Vector(args);
+            bothargs.addAll(testset.args);
+            List argcombo = argcombo(bothargs);
+            argcombos.add(new Integer(argcombo.size()));
+            testsetToArgcombo.put(testset, argcombo);
+        }
+        while (!testsetToArgcombo.isEmpty()) {
+            int _ = 1;
+            for (Iterator iter = testsets.iterator(); iter.hasNext(); _++) {
+                Testset testset = (Testset)iter.next();
+                List argcombo = (List)testsetToArgcombo.get(testset);
+                if (argcombo.size() == 0) {
+                    testsetToArgcombo.remove(testset);
+                    continue;
+                }
+                List args = (List)argcombo.remove(0);
+                final String startStr = "Testset " + _ + " of " + testsets.size();
+                String str = startStr + " / Combo " + _ + " of " + argcombos.size();
+                log("---------- " + str + " ----------");
+                execute(testset, args);
+            }
+        }
+        
+//          for (Iterator iter = testsets.iterator(); iter.hasNext(); _++) {
+//              Testset testset = (Testset)iter.next();
+//              testset.resolve();
+//              List bothargs = new Vector(args);
+//              bothargs.addAll(testset.args);
+//              int __ = 1;
+//              List argcombo = argcombo(bothargs);
+//              log(argcombo.size() + " combination" + s(argcombo),
+//                  Project.MSG_VERBOSE);
+//              final String startStr = "Testset " + _ + " of " + testsets.size();
+//              for (Iterator comboiter = argcombo.iterator();
+//                   comboiter.hasNext(); __++) {
+//                  List args = (List)comboiter.next();
+//                  execute(testset, args);
+//                  log("");
+//              }
+//          }        
+        finish();
+    }
+
+    private void delete(File dir) throws BuildException {
+        Delete delete = (Delete)project.createTask("delete");
+        delete.setDir(dir);
+        delete.execute();
+    }
+
+    private void make(File dir) throws BuildException {
+        Mkdir mkdir = (Mkdir)project.createTask("mkdir");
+        mkdir.setDir(dir);
+        mkdir.execute();
+    }
+
+    private String getAntProperty(String name) {
+        String uprop = project.getUserProperty(name);
+        if (null == uprop) {
+            uprop = project.getProperty(name);
+        }
+        return uprop;
+    }
+
+    private boolean isSet(String name) {
+        String uprop = project.getUserProperty(name);
+        if (uprop == null ||
+            "no".equals(uprop) ||
+            "false".equals(uprop)) return false;
+        String prop = project.getProperty(name);
+        if (prop == null ||
+            "no".equals(prop) ||
+            "false".equals(prop)) return false;
+        return true;
+    }
+
+    /** 
+     * Interpose Wrapper class to catch and report exceptions
+     * by setting a positive value for System.exit().
+     * (In some cases it seems that Exceptions are not being reported
+     *  as errors in the tests.)
+     * This forces the VM to fork.  A forked VM is required for
+     * two reasons:
+     * (1) The wrapper class may have been defined by a different 
+     * class loader than the target class, so it would not be able
+     * to load the target class;
+     * <p>
+     * (2) Since the wrapper class is generic, we have to pass in
+     * the name of the target class.  I choose to do this using
+     * VM properties rather than hacking up the arguments.  
+     * <p>todo: relies on name/value of property "taskdef.jar"
+     *    to add jar with wrapper to invoking classpath.
+     * <p>
+     * It is beneficial for another reason:
+     * (3) The wrapper class can be asked to test-load all classes
+     *     in a classes dir, by setting a VM property.  This class
+     *     sets up the property if the value is defined using
+     *     <code>setClassesDir(String)</code>
+     * <p>todo: if more tunnelling, generalize and parse.
+     */
+    public class RunWrapper extends Java {
+        public final Class LINK_WRAPPER_CLASS = MainWrapper.class;
+        /** tracked in MainWrapper.PROP_NAME */ // todo: since reflective, avoid direct
+        public final String PROP_NAME = "MainWrapper.classname";
+        /** tracked in MainWrapper.CLASSDIR_NAME */
+        public final String CLASSDIR_NAME = "MainWrapper.classdir";
+        public final String WRAPPER_CLASS 
+            = "org.aspectj.internal.tools.ant.taskdefs.MainWrapper";
+        private String classname;
+        protected String classesDir;
+        /** capture classname here, replace with WRAPPER_CLASS */
+        public void setClassname(String classname) {
+            super.setClassname(WRAPPER_CLASS);
+            this.classname = classname;
+        }
+
+        /**
+         * Setup the requirements for the wrapper class:
+         * <li>fork to get classpath and VM properties right</li>
+         * <li>set VM property</li>
+         * <li>add ${ajctest.wrapper.jar} (with wrapper class) to the classpath</li>
+         */
+        private void setup() {
+            setFork(true);
+            Commandline.Argument cname = createJvmarg();
+            cname.setValue("-D"+PROP_NAME+"="+classname);
+            if (!noverify) {
+                cname = createJvmarg();
+                cname.setValue("-Xfuture"); // todo: 1.2 or later..
+            }
+            if (null != classesDir) {
+                cname = createJvmarg();
+                cname.setValue("-D"+CLASSDIR_NAME+"="+classesDir);
+            }
+            // todo dependence on name/setting of ajctest.wrapper.jar
+            String value =  project.getProperty("ajctest.wrapper.jar");
+            if (null != value) {
+                Path wrapperPath = new Path(project, value);
+               RunWrapper.this.createClasspath().append(wrapperPath);
+            }
+        }
+        
+        /** do setup, then super.execute() */
+        public int executeJava() {
+            setup();
+            int result = super.executeJava();
+            // snarf - also load all classes?
+            return result;
+        }
+
+        /** set directory to scan for classes */
+        public void setClassesDir(String dir) {
+            classesDir = dir;
+        }
+    }
+
+    public class Run extends RunWrapper {
+        //public class Run extends Java 
+        private Path bootclasspath;
+        public void setBootbootclasspath(Path path) {
+            if (bootclasspath == null) {
+                bootclasspath = path;
+            } else {
+                bootclasspath.append(path);
+            }
+        }
+        public Path createBootbootclasspath() {
+            if (bootclasspath == null) bootclasspath = new Path(this.project);
+            return bootclasspath.createPath();
+        }
+        public void setBootbootclasspathRef(Reference r) {
+            createBootbootclasspath().setRefid(r);
+        }
+        private Path bootclasspatha;
+        public void setBootbootclasspatha(Path path) {
+            if (bootclasspatha == null) {
+                bootclasspatha = path;
+            } else {
+                bootclasspatha.append(path);
+            }
+        }
+        public Path createBootbootclasspatha() {
+            if (bootclasspatha == null) bootclasspatha = new Path(this.project);
+            return bootclasspatha.createPath();
+        }
+        public void setBootbootclasspathaRef(Reference r) {
+            createBootbootclasspatha().setRefid(r);
+        }
+        private Path bootclasspathp;
+        public void setBootbootclasspathp(Path path) {
+            if (bootclasspathp == null) {
+                bootclasspathp = path;
+            } else {
+                bootclasspathp.append(path);
+            }
+        }
+        public Path createBootbootclasspathp() {
+            if (bootclasspathp == null) bootclasspathp = new Path(this.project);
+            return bootclasspathp.createPath();
+        }
+        public void setBootbootclasspathpRef(Reference r) {
+            createBootbootclasspathp().setRefid(r);
+        }
+        public Run(Project project) {
+            super();
+            //this.project = Ajctest.this.project;
+            this.setTaskName("ajcjava");
+            this.project = project;
+        }
+        public String msgs = "";
+        public int executeJava() {
+            Path cp = Ajctest.this.classpath != null ? Ajctest.this.classpath :
+                new Path(this.project, destdir.getAbsolutePath());
+            cp.append(Path.systemClasspath);
+            this.setClasspath(cp);
+            if (bootclasspath != null) {
+                setFork(true);
+                createJvmarg().setValue("-Xbootclasspath:" + bootclasspath);
+            }
+            if (bootclasspatha != null) {
+                setFork(true);
+                createJvmarg().setValue("-Xbootclasspath/a:" + bootclasspatha);
+            }
+            if (bootclasspathp != null) {
+                setFork(true);
+                createJvmarg().setValue("-Xbootclasspath/p:" + bootclasspathp);
+            }
+            int exit = -1;
+            // todo: add timeout feature todo: this or below?
+            try {
+                exit = super.executeJava();
+            } catch (Throwable t) {
+                StringWriter sw = new StringWriter();
+                PrintWriter out = new PrintWriter(sw);
+                t.printStackTrace(out);
+                msgs = sw.toString();
+                out.close();
+                // todo: return exit code
+            }
+            return exit;
+        }
+        public String _classname;
+        public String classname() { return _classname; }
+        public void setClassname(String classname) {
+            super.setClassname(_classname = classname);
+        }
+        public String toString() { return _classname; }
+    }
+    // class Run
+    // todo: need to run in a wrapper which report non-zero int on exception
+    // todo: unused method? see executeJava above.
+    private int java(String classname, Collection args) throws BuildException {
+        Java java = (Java)project.createTask("java");
+        java.setClassname(classname);
+        for (Iterator i = args.iterator(); i.hasNext();) {
+            Object o = i.next();
+            Commandline.Argument arg = java.createArg();
+            if (o instanceof File) {
+                arg.setFile((File)o);
+            } else if (o instanceof Path) {
+                arg.setPath((Path)o);
+            } else {
+                arg.setValue(o+"");
+            }
+        }
+        return java.executeJava();
+    }
+
+    private static List allErrors = new Vector();
+    private List errors = new Vector();
+
+    private void post(Testset testset, List args,
+                      String msgs, int exit, String type) {
+        errors.add(new Failure(testset, args, msgs, exit, type, testId));
+        fire(type + ".fail");
+    }
+
+    private static long startTime;
+    private static long stopTime;
+
+    private static String date(long time) {
+        return DateFormat.getDateTimeInstance
+            (DateFormat.FULL, DateFormat.FULL).
+            format(new Date(time));
+    }
+
+    static {
+        final PrintStream err = System.err;
+        startTime = System.currentTimeMillis();
+        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+                private String ms(long start, long stop) {
+                    long rest = Math.abs(stop-start) / 1000;
+                    long days = rest / 86400;
+                    long hours = (rest -= days*86400) / 3600;
+                    long mins = (rest -= hours*3600) / 60;
+                    long secs = (rest -= mins*60);
+                    boolean req = false;
+                    String str = "";
+                    if (req || days > 0) {
+                        req = true;
+                        str += days + " day" + (days != 1 ? "s" : "") + " ";
+                    }
+                    if (req || hours > 0) {
+                        req = true;
+                        str += hours + " hour" + (hours != 1 ? "s" : "") + " ";
+                    }
+                    if (req || mins > 0) {
+                        req = true;
+                        str += mins + " minute" + (mins != 1 ? "s" : "") + " ";
+                    }
+                    str += secs + " second" + (secs != 1 ? "s" : "") + " ";
+                    return str;
+                }
+               
+                public void run() {
+                    Ajctest current = CURRENT_AJCTEST;
+                    String oneLine = "warning: oneLine not set.";
+                    String multiLine = "warning: multiLine not set.";
+                    
+                    // setup oneLine
+                    if (null == current) {
+                        oneLine = "\nRESULT=\"ERROR\" null ACJTEST";
+                    } else {
+                        StringBuffer sb = new StringBuffer("\n");
+                        int errs = current.allErrors.size();
+                        int allFails = errs 
+                            + current.ajdocStats.fails
+                            + current.ajcStats.fails
+                            + current.runStats.fails;
+                        if (1 > allFails) {
+                            sb.append("RESULT=\"PASS\"\terrors=\"");
+                        } else {
+                            sb.append("RESULT=\"FAIL\"\terrors=\"");
+                        }
+                        sb.append(""+errs);
+                        sb.append("\"\tajdoc.pass=\"");
+                        sb.append(""+current.ajdocStats.goods);
+                        sb.append("\"\tajdoc.fail=\"");
+                        sb.append(""+current.ajdocStats.fails);
+                        sb.append("\"\tajc.pass=\"");
+                        sb.append(""+current.ajcStats.goods);
+                        sb.append("\"\tajc.fail=\"");
+                        sb.append(""+current.ajcStats.fails);
+                        sb.append("\"\trun.pass=\"");
+                        sb.append(""+current.runStats.goods);
+                        sb.append("\"\trun.fail=\"");
+                        sb.append(""+current.runStats.fails);
+                        sb.append("\"\ttestId=\"");
+                        sb.append(current.testId);
+                        sb.append("\"\tproject=\"");
+                        Project p = current.getProject();
+                        if (null != p) sb.append(p.getName());
+                        sb.append("\"\tfile=\"");
+                        sb.append(""+current.getLocation());
+                        sb.append("\"");
+                        oneLine = sb.toString();
+                    }
+
+                    // setup multiLine
+                    {
+                        stopTime = System.currentTimeMillis();
+                        String str = "";
+                        str += "\n";
+                        str +=
+                            "===================================" +
+                            "===================================" + "\n";
+                        str += "Test started : " + date(startTime) + "\n";
+                        str += "Test ended   : " + date(stopTime) + "\n";
+                        str += "Total time   : " + ms(startTime, stopTime) + "\n";
+                        str +=
+                            "------------------------------" +
+                            " Summary " + 
+                            "------------------------------" + "\n";
+                        str += "Task\tPassed\tFailed" + "\n";
+                        Object[] os = new Object[] {
+                            "ajdoc", ajdocStats.goods+"",   ajdocStats.fails+"",
+                            "ajc",   ajcStats.goods  +"",   ajcStats.fails  +"",
+                            "run",   runStats.goods  +"",   runStats.fails  +"",
+                        };
+                        for (int i = 0; i < os.length; i += 3) {
+                            str += os[i] + "\t" + os[i+1] + "\t" + os[i+2] + "\n";
+                        }
+                        if (allErrors.size() > 0) {
+                            str += "" + "\n";
+                            str += 
+                                "There " + w(allErrors) + " " +
+                                allErrors.size() + " error" +
+                                s(allErrors) + ":" + "\n";
+                            for (int i = 0; i < allErrors.size(); i++) {
+                                Failure failure = (Failure)allErrors.get(i);
+                                str +=
+                                    "---------- Error " + i + " [" +
+                                    failure.testId + "]" +
+                                    " ------------------------------" + "\n";
+                                str += " " + failure + "\n\n";
+                            }
+                        } else {
+                            str += "No errors." + "\n";
+                        }
+                        str += "--------------------------" +
+                            " End of Summary " + 
+                            "---------------------------" + "\n";
+                        multiLine = str;
+                    }
+
+                    // print both multiLine and oneLine
+                    err.println(multiLine);
+                    err.println(oneLine);
+                    if (dumpresults && (allErrors.size() +
+                                        ajdocStats.fails +
+                                        ajcStats.fails   +
+                                        runStats.fails) > 0) {
+                        String date = date(System.currentTimeMillis());
+                        String filename = "ajc-errors";
+                        for (StringTokenizer t = new StringTokenizer(date, ",: ");
+                             t.hasMoreTokens();) {
+                            filename += "-" + t.nextToken().trim();
+                        }
+                        filename += ".txt";
+                        PrintWriter out = null;
+                        File file = new File(filename);
+                        System.err.println("dumping results to " + file);
+                        try {
+                            out = new PrintWriter(new FileWriter(file));
+                            out.println(multiLine);
+                            out.println(oneLine);
+                            System.err.println("dumped results to " + file);
+                        } catch (IOException ioe) {
+                            if (out != null) out.close();
+                        }
+                    }
+                }
+            }));
+    }
+
+    private static String w(List list) { return a(list, "were", "was"); }
+    private static String s(List list) { return a(list, "s", ""); }
+
+    private static String a(List list, String some, String one) {
+        return list == null || list.size() != 1 ? some : one;
+    }
+    
+    static class Failure {
+        public final Testset testset;
+        public final List args;
+        public final String msgs;
+        public final int exit;
+        public final String type;
+        public final long time;
+        public final String testId;
+        public Failure(Testset testset, List args,
+                       String msgs, int exit, String type,
+                       String testId) {
+            this.testset = testset;
+            this.args = args;
+            this.msgs = msgs;
+            this.exit = exit;
+            this.type = type;
+            this.time = System.currentTimeMillis();
+            this.testId = testId;
+        }
+        public String toString() {
+            String str = "testId:" + testId+ "\n";
+            str += "type:" + type + "\n";
+            str += testset + "\n";
+            if (args.size() > 0) {
+                str += " args: " + args + "\n";;
+            }
+            str += " msgs:" + msgs + "\n";
+            str += " exit:" + exit;
+            return str;
+        }
+    }
+
+    private List argcombo(List arguments) {
+        List combos = new Vector();
+        List always = new Vector();
+        for (Iterator iter = arguments.iterator(); iter.hasNext();) {
+            Argument arg = (Argument)iter.next();
+            if (arg.values.size() == 0) arg.values.add("");
+            if (!arg.always && !arg.values.contains(null)) arg.values.add(null);
+            if (arg.values.size() > 0) {
+                combos.add(arg);
+            } else if (arg.always) {
+                always.add(new Arg(arg.name, arg.values.get(0)+"", arg.isj));
+            }
+        }
+        List argcombo = combinations(combos);
+        for (Iterator iter = always.iterator(); iter.hasNext();) {
+            Arg arg = (Arg)iter.next();
+            for (Iterator comboiter = argcombo.iterator(); comboiter.hasNext();) {
+                ((List)comboiter.next()).add(arg);
+            }
+        }
+        return argcombo;
+    }
+
+    private abstract class ExecWrapper {
+        public String msgs;
+        public int run() {
+            return run(createCommandline());
+        }
+        protected abstract Commandline createCommandline();
+        protected final int run(Commandline cmd) {
+            Process process = null;
+            int exit = Integer.MIN_VALUE;
+            final StringBuffer buf = new StringBuffer();
+            Thread errPumper = null;
+            Thread outPumper = null;
+            try {
+                log("\tcalling " + cmd, Project.MSG_VERBOSE);
+                process = Runtime.getRuntime().exec(cmd.getCommandline());
+                OutputStream os = new OutputStream() {
+                        StringBuffer sb = new StringBuffer();
+                        public void write(int b) throws IOException {
+                            final char c = (char)b;
+                            buf.append(c);
+                            if (c != '\n') {
+                                sb.append(c);
+                            } else {
+                                System.err.println(sb.toString());
+                                sb = new StringBuffer();
+                            }
+                        }
+                    };
+                OutputStream los = new LogOutputStream(Ajctest.this,
+                                                       Project.MSG_INFO);
+                outPumper = new Thread(new StreamPumper(process.getInputStream(),
+                                                        los));
+                errPumper = new Thread(new StreamPumper(process.getErrorStream(),
+                                                        os));
+                outPumper.setDaemon(true);
+                errPumper.setDaemon(true);
+                outPumper.start();
+                errPumper.start();
+                process.waitFor();
+            } catch (Exception e) {
+                e.printStackTrace();
+                //throw e;
+            } finally {
+                try {
+                    if (outPumper != null) outPumper.join();
+                    if (errPumper != null) errPumper.join();
+                } catch (InterruptedException ie) {
+                } finally {
+                    outPumper = null;
+                    errPumper = null;
+                }
+                exit = process.exitValue();
+                msgs = buf.toString();
+                if (exit != 0) {
+                    log("Test failed with exit value: " + exit);
+                } else {
+                    log("Success!", Project.MSG_VERBOSE);
+                }
+                if (process != null) process.destroy();
+                process = null;
+                System.err.flush();
+                System.out.flush();
+            }
+            return exit;
+        }
+    }
+
+    private class AjcWrapper extends JavaCommandWrapper {
+        public AjcWrapper(Testset testset, List args) {
+            super(testset, args, false);
+            if (testset.noclean) {
+                setExtraclasspath(new Path(project, 
+                                           destdir.getAbsolutePath()));
+            }
+        }
+        String getMainClassName() {
+            return "org.aspectj.tools.ajc.Main";
+        }
+    }
+
+    private class EAjcWrapper extends JavaCommandWrapper {
+        public EAjcWrapper(Testset testset, List args) {
+            super(testset, args, false);
+            if (testset.noclean) {
+                setExtraclasspath(new Path(project, 
+                                           destdir.getAbsolutePath()));
+            }
+        }
+        String getMainClassName() {
+            return "org.aspectj.ajdt.ajc.Main";
+        }
+    }
+
+    static List ajdocArgs(List args) {
+        List newargs = new Vector();
+        for (Iterator i = args.iterator(); i.hasNext();) {
+            String arg = i.next() + "";
+            if (arg.startsWith("-X")) {
+                newargs.add(arg);
+            } else if (arg.equals("-public")    ||
+                       arg.equals("-package")   ||
+                       arg.equals("-protected") ||
+                       arg.equals("-private")) {
+                newargs.add(arg);
+            } else if (arg.equals("-d")             ||
+                       arg.equals("-classpath")     ||
+                       arg.equals("-cp")            ||
+                       arg.equals("-sourcepath")    ||
+                       arg.equals("-bootclasspath") ||
+                       arg.equals("-argfile")) {
+                newargs.add(arg);
+                newargs.add(i.next()+"");
+            } else if (arg.startsWith("@")) {
+                newargs.add(arg);
+            }
+        }
+        return newargs;
+    }
+
+    private class AjdocWrapper extends JavaCommandWrapper {
+        public AjdocWrapper(Testset testset, List args) {
+            super(testset, ajdocArgs(args), true);
+            String[] cmds = testset.getAjdoc().getCommandline().getCommandline();
+            for (int i = 0; i < cmds.length; i++) {
+                this.args.add(cmds[i]);
+            }
+        }
+        String getMainClassName() {
+            return "org.aspectj.tools.ajdoc.Main";
+        }
+    }
+
+    private abstract class JavaCommandWrapper extends ExecWrapper {
+        abstract String getMainClassName();
+        protected Testset testset;
+        protected List args;
+        protected boolean needsClasspath;
+        protected Path extraClasspath;
+        
+        public JavaCommandWrapper(Testset testset, List args,
+                                  boolean needsClasspath) {
+            this.testset = testset;
+            this.args = args;
+            this.needsClasspath = needsClasspath;
+            this.extraClasspath = testset.internalclasspath;
+        }
+        public void setExtraclasspath(Path extraClasspath) {
+            this.extraClasspath = extraClasspath;
+        }
+        
+        public String toString() {
+            return LangUtil.unqualifiedClassName(getClass()) 
+                + "(" + getMainClassName() + ")";
+        }
+        
+        protected Commandline createCommandline() {
+            Commandline cmd = new Commandline();
+            cmd.setExecutable("java");
+            cmd.createArgument().setValue("-classpath");
+            Path cp = null;
+            if (extraClasspath != null) {
+                cp = extraClasspath;
+            }
+            if (extraClasspath == null) {
+                Path aspectjBuildDir = 
+                    new Path(project,
+                             project.getProperty("ajctest.pathelement"));
+                // todo: dependency on ant script variable name ajctest.pathelement
+                if (cp == null) cp = aspectjBuildDir;
+                else cp.append(aspectjBuildDir);
+            }
+            if (cp == null) {
+                cp = Path.systemClasspath;
+            } else {
+                cp.append(Path.systemClasspath);
+            }
+            cmd.createArgument().setPath(cp);
+            for (Iterator iter = args.iterator(); iter.hasNext();) {
+                Arg arg = (Arg)iter.next();
+                if (arg.isj) {
+                    cmd.createArgument().setValue(arg.name);
+                    if (!arg.value.equals("")) {
+                        cmd.createArgument().setValue(arg.value);
+                    }
+                }
+            }
+            cmd.createArgument().setValue(getMainClassName());
+            boolean alreadySetDestDir = false;
+            boolean alreadySetClasspath = false;
+            for (Iterator iter = args.iterator(); iter.hasNext();) {
+                Arg arg = (Arg)iter.next();
+                if (!arg.isj) {
+                    cmd.createArgument().setValue(arg.name);
+                    if (arg.name.equals("-d")) {
+                        setDestdir(arg.value+"");
+                        alreadySetDestDir = true;
+                    }
+                    if (arg.name.equals("-classpath")) {
+                        alreadySetClasspath = true;
+                    }
+                    if (!arg.value.equals("")) {
+                        cmd.createArgument().setValue(arg.value);
+                    }
+                }
+            }
+            if (destdir == null) {
+                setDestdir(".");
+            }
+            if (!alreadySetDestDir) {
+                cmd.createArgument().setValue("-d");
+                cmd.createArgument().setFile(destdir);
+            }
+            if (!alreadySetClasspath && testset.classpath != null) {
+                cmd.createArgument().setValue("-classpath");
+                cmd.createArgument().setPath(testset.classpath);
+            } else if (needsClasspath) {
+                Path _cp = Ajctest.this.classpath != null ? Ajctest.this.classpath :
+                    new Path(project, destdir.getAbsolutePath());
+                _cp.append(Path.systemClasspath);
+                cmd.createArgument().setValue("-classpath");
+                cmd.createArgument().setPath(_cp);
+            }
+            for (Iterator iter = testset.files.iterator(); iter.hasNext();) {
+                cmd.createArgument().setFile((File)iter.next());
+            }
+            for (Iterator iter = testset.argfiles.iterator(); iter.hasNext();) {
+                cmd.createArgument().setValue("-argfile");
+                cmd.createArgument().setFile((File)iter.next());
+            }
+            return cmd;
+        }
+    }
+
+    /** implement invocation of ajc */
+    private void java(Testset testset, List args) throws BuildException {
+        Java java = (Java)project.createTask("java");
+        java.setClassname("org.aspectj.tools.ajc.Main");
+        if (classpath != null) {
+            java.setClasspath(classpath);
+        }
+        for (Iterator iter = args.iterator(); iter.hasNext();) {
+            Arg arg = (Arg)iter.next();
+            if (arg.isj) {
+                java.createJvmarg().setValue(arg.name);
+                if (!arg.value.equals("")) {
+                    java.createJvmarg().setValue(arg.value);
+                }
+            }
+        }
+        for (Iterator iter = args.iterator(); iter.hasNext();) {
+            Arg arg = (Arg)iter.next();
+            if (!arg.isj) {
+                java.createArg().setValue(arg.name);
+                if (!arg.value.equals("")) {
+                    java.createArg().setValue(arg.value);
+                }
+            }
+        }
+        for (Iterator iter = testset.files.iterator(); iter.hasNext();) {
+            java.createArg().setFile((File)iter.next());
+        }
+        for (Iterator iter = testset.argfiles.iterator(); iter.hasNext();) {
+            java.createArg().setValue("-argfile");
+            java.createArg().setFile((File)iter.next());
+        }
+        java.setFork(true);
+        java.execute();
+    }
+
+    private void exec(Testset testset, List args) throws BuildException {
+        ExecTask exec = (ExecTask)project.createTask("exec");
+        exec.setExecutable("java");
+        if (classpath != null) {
+            exec.createArg().setValue("-classpath");
+            exec.createArg().setPath(classpath);
+        }
+        for (Iterator iter = args.iterator(); iter.hasNext();) {
+            Arg arg = (Arg)iter.next();
+            if (arg.isj) {
+                exec.createArg().setValue(arg.name);
+                if (!arg.value.equals("")) {
+                    exec.createArg().setValue(arg.value);
+                }
+            }
+        }
+        exec.createArg().setValue("org.aspectj.tools.ajc.Main");
+        for (Iterator iter = args.iterator(); iter.hasNext();) {
+            Arg arg = (Arg)iter.next();
+            if (!arg.isj) {
+                exec.createArg().setValue(arg.name);
+                if (!arg.value.equals("")) {
+                    exec.createArg().setValue(arg.value);
+                }
+            }
+        }
+        for (Iterator iter = testset.files.iterator(); iter.hasNext();) {
+            exec.createArg().setFile((File)iter.next());
+        }
+        for (Iterator iter = testset.argfiles.iterator(); iter.hasNext();) {
+            exec.createArg().setValue("-argfile");
+            exec.createArg().setFile((File)iter.next());
+        }
+        exec.execute();
+    }
+    
+    public void handle(Throwable t) {
+        log("handling " + t);
+        if (t != null) t.printStackTrace();
+        log("done handling " + t);
+    }
+
+    private List combinations(List arglist) {
+        List result = new Vector();
+        result.add(new Vector());
+        for (Iterator iter = arglist.iterator(); iter.hasNext();) {
+            Argument arg = (Argument)iter.next();
+            int N = result.size();
+            for (int i = 0; i < N; i++) {
+                List to = (List)result.remove(0);
+                for (Iterator valiter = arg.values.iterator(); valiter.hasNext();) {
+                    List newlist = new Vector(to);
+                    Object val = valiter.next();
+                    if (val != null) newlist.add(new Arg(arg.name, val+"", arg.isj));
+                    result.add(newlist);
+                }
+            }
+        }
+        return result;
+    }
+
+    /////////////////////// GUI support //////////////////////////////
+    private static Gui gui;
+
+    private static void gui(Ajctest ajc) {
+        if (firstTime && ajc.isSet("gui")) {
+            JFrame f = new JFrame("AspectJ Test Suite");
+            f.getContentPane().add(BorderLayout.CENTER, gui = new Gui());
+            f.pack();
+            f.setVisible(true);
+        }
+        if (gui != null) {
+            ajc.bean.addPropertyChangeListener(gui);
+        }
+        firstTime = false;
+    }
+
+    private static class Gui extends JPanel implements PropertyChangeListener {
+        private FailurePanel fail = new FailurePanel();
+        private TablePanel table = new TablePanel();
+        private StatusPanel status = new StatusPanel();
+        public Gui() {
+            super(new BorderLayout());
+            JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+            split.setPreferredSize(new Dimension(500, 300));
+            split.add(JSplitPane.BOTTOM, fail);
+            split.add(JSplitPane.TOP, table);
+            split.setDividerLocation(200);
+            add(BorderLayout.CENTER, split);
+            add(BorderLayout.SOUTH, status);
+            setPreferredSize(new Dimension(640, 680));
+        }
+        public void propertyChange(PropertyChangeEvent evt) {
+            String name = evt.getPropertyName();
+            if ("ajdoc.good".equals(name)) {
+                status.ajc.goods.inc();
+            } else if ("ajc.good".equals(name)) {
+                status.ajc.goods.inc();
+            } else if ("run.good".equals(name)) {
+                status.runs.goods.inc();
+            }
+            if ("ajdoc.done".equals(name)) {
+                status.ajc.total.inc();
+            } else if ("ajc.done".equals(name)) {
+                status.ajc.total.inc();
+            } else if ("run.done".equals(name)) {
+                status.runs.total.inc();
+            }
+            if ("ajdoc.fail".equals(name)) {
+                status.ajc.fails.inc();
+            } else if ("ajc.fail".equals(name)) {
+                status.ajc.fails.inc();
+            } else if ("run.fail".equals(name)) {
+                status.runs.fails.inc();
+            }
+        }
+        
+        private abstract static class TitledPanel extends JPanel {
+            public TitledPanel(LayoutManager layout, String title) {
+                super(layout);
+                setBorder(BorderFactory.createTitledBorder(title));
+            }
+        }
+
+        private static class StatusPanel extends TitledPanel {
+            StatusInnerPanel ajdoc = new StatusInnerPanel("Ajdoc");
+            StatusInnerPanel runs  = new StatusInnerPanel("Runs");
+            StatusInnerPanel ajc   = new StatusInnerPanel("Ajc");
+
+            public StatusPanel() {
+                super(new FlowLayout(), "Status");
+                add(ajdoc);
+                add(runs);
+                add(ajc);
+            }
+
+            private static class StatusInnerPanel extends TitledPanel {
+                IntField goods = new IntField(5, Color.green.darker());
+                IntField fails = new IntField(5, Color.red.darker());
+                IntField total = new IntField(5, Color.blue.darker());
+                public StatusInnerPanel(String str) {
+                    super(null, str);
+                    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+                    Object[] os = new Object[] {
+                        "Passed", goods,
+                        "Failed", fails,
+                        "Totals", total,
+                    };
+                    for (int i = 0; i < os.length; i += 2) {
+                        JPanel p = p();
+                        p.add(new JLabel(os[i]+":"));
+                        p.add((Component)os[i+1]);
+                        this.add(p);
+                    }
+                }
+
+                private JPanel p() {
+                    JPanel p = new JPanel();
+                    p.setLayout(new FlowLayout(FlowLayout.LEFT));
+                    return p;
+                }
+
+                private class IntField extends JTextField {
+                    public IntField(int i, Color fg) {
+                        super("0", i);
+                        this.setBackground(StatusInnerPanel.this.getBackground());
+                        this.setForeground(fg);
+                        this.setEditable(false);
+                        this.setBorder(BorderFactory.createEmptyBorder());
+                    }
+                    public void add(int i) {
+                        setText((Integer.parseInt(getText().trim())+i)+"");
+                    }
+                    public void inc() { add(1); }
+                }
+            }
+        }
+        
+        private class TablePanel extends TitledPanel {
+            private DefaultTableModel model = new DefaultTableModel();
+            private TJable table;
+            private List failures = new Vector();
+            public TablePanel() {
+                super(new BorderLayout(), "Failures");
+                Object[] names = new String[] {
+                    "Task", "Type", "Number", "Time"
+                };
+                for (int i = 0; i < names.length; i++) {
+                    model.addColumn(names[i]);
+                }
+                table = new TJable(model, failures);
+                this.add(new JScrollPane(table), BorderLayout.CENTER);
+            }
+
+            private class TJable extends JTable {
+                private List list;
+                public TJable(TableModel model, List list) {
+                    super(model);
+                    this.list = list;
+                    setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+                }
+                public void valueChanged(ListSelectionEvent e) {
+                    super.valueChanged(e);
+                    if (list == null) return;
+                    int i = (e.getFirstIndex()-e.getLastIndex())/2;
+                    if (list.size() > 0) {
+                        Failure f = (Failure)list.get(i);
+                        fail.setFailure(f);
+                    }
+                }
+            }
+
+            public void add(Failure f, String taskname, String type,
+                            int num, long time) {
+                model.addRow(new Object[]{taskname, type,
+                                          new Integer(num), date(time)});
+                failures.add(f);
+            }
+        }
+
+        private static class FailurePanel extends TitledPanel {
+            private JTextArea msgs = new JTextArea(10,50);
+            private InfoPanel info = new InfoPanel();
+
+            public FailurePanel() {
+                super(new BorderLayout(), "Failure");
+                msgs.setFont(StyleContext.getDefaultStyleContext().
+                             getFont("SansSerif", Font.PLAIN, 10));
+                add(BorderLayout.NORTH, info);
+                JScrollPane sc = new JScrollPane(msgs);
+                sc.setBorder(BorderFactory.createTitledBorder("Messages"));
+                add(BorderLayout.CENTER, sc);
+            }
+
+            public void setText(String str) {
+                msgs.setText(str);
+            }
+
+            public void setFailure(Failure f) {
+                msgs.setText(f.msgs);
+                info.setText("Type"        , f.type);
+                info.setText("Args"        , f.args);
+                info.setText("Exit"        , f.exit+"");
+                info.setText("Time"        , date(f.time));
+                info.setText("Files"       , f.testset.files);
+                info.setText("Classnames"  , f.testset.testclasses);
+            }
+
+            private static class InfoPanel extends JPanel {
+                Map fields = new HashMap();
+                public void setText(String key, Object str) {
+                    ((JTextField)fields.get(key)).setText(str+"");
+                }
+                public InfoPanel() {
+                    super(new GridBagLayout());
+                    LabelFieldGBC gbc = new LabelFieldGBC();
+                    Object[] os = new Object[] {
+                        "Type",
+                        "Args",
+                        "Exit",
+                        "Time",
+                        "Files",
+                        "Classnames",
+                    };
+                    for (int i = 0; i < os.length; i++) {
+                        String name = os[i]+"";
+                        JLabel label = new JLabel(name+":");
+                        JTextField comp = new JTextField(25);
+                        comp.setEditable(false);
+                        comp.setBackground(Color.white);
+                        comp.setBorder(BorderFactory.
+                                       createBevelBorder(BevelBorder.LOWERED));
+                        label.setLabelFor(comp);
+                        fields.put(name, comp);
+                        add(label, gbc.forLabel());
+                        add(comp, gbc.forField());
+                    }
+                    add(new JLabel(), gbc.forLastLabel());
+                }
+            }
+
+            private static class LabelFieldGBC extends GridBagConstraints {
+                public LabelFieldGBC() {
+                    insets = new Insets(1,3,1,3);
+                    gridy = RELATIVE;
+                    gridheight = 1;
+                    gridwidth = 1;
+                }
+                public LabelFieldGBC forLabel() {
+                    fill = NONE;
+                    gridx = 0;
+                    anchor = NORTHEAST;
+                    weightx = 0.0;
+                    return this;
+                }
+
+                public LabelFieldGBC forLastLabel() {
+                    forLabel();
+                    fill = VERTICAL;
+                    weighty = 1.0;
+                    return this;
+                }
+
+                public LabelFieldGBC forField() {
+                    fill = HORIZONTAL;
+                    gridx = 1;
+                    anchor = CENTER;
+                    weightx = 1.0;
+                    return this;
+                }
+
+                public LabelFieldGBC forLastField() {
+                    forField();
+                    fill = BOTH;
+                    weighty = 1.0;
+                    return this;
+                }
+            }
+        }
+    }
+
+}
diff --git a/testing/src/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java b/testing/src/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java
new file mode 100644 (file)
index 0000000..656a284
--- /dev/null
@@ -0,0 +1,181 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+
+package org.aspectj.internal.tools.ant.taskdefs;
+
+import org.aspectj.testing.util.LangUtil; // todo config management here
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.InvocationTargetException;
+import java.io.*;
+
+/** 
+ * Wrapper to invoke class identified by setting VM argument.
+ * Caller must set a system property "MainWrapper.classname"
+ * to the fully-qualified name of the target class to invoke,
+ * and the target class must be resolvable from the defining
+ * class loader of this class.
+ * VM argument name is available as <code>PROP_NAME</code>, but
+ * is set by adding the following to the command line:
+ * <code>-DMainWrapper.classname="fully.qualified.Classname"</code>.
+ * This returns -1 if unable to load the main method,
+ * 1 if the invoked method throws an exception, and 0 otherwise.
+ * TODO: prelminary
+ */
+public class MainWrapper {
+    /** MUST set the fully-qualified name of class to invoke using
+     * a VM property of this name 
+     * tracked in Ajctest.java */
+    public static final String PROP_NAME = "MainWrapper.classname";
+    /** May set the path to a classes diretory, 
+     * to interpret class names and load classes.
+     * Tracked in Ajctest.java */
+    public static final String CLASSDIR_NAME = "MainWrapper.classdir";
+
+    /** to disable returning int via System.exit, set to boolean true value (todo: ignored) */
+    public static final String SIGNAL_EXCEPTION_NAME = "MainWrapper.signalException";
+
+    /** to disable returning via System.exit on first Throwable, set to boolean true value (todo: ignored) */
+    public static final String FAIL_ON_EXCEPTION_NAME = "MainWrapper.failOnException";
+
+    /** quit on first exception */ // todo public class controls - yuck 
+    public static boolean FAIL_ON_EXCEPTION = true;
+
+    /** signal number of exceptions with int return value */ 
+    public static boolean SIGNAL_EXCEPTION = true;
+
+    /** redirect messages for exceptions; if null, none printed */ 
+    public static PrintStream OUT_STREAM = System.err;
+
+    /** result accumulated, possibly from multiple threads */ 
+    private static int result;
+
+    /**
+     * Run target class's main(args), doing a System.exit() with
+     * a value > 0 for the number of Throwable that 
+     *  the target class threw that
+     * makes it through to a top-level ThreadGroup. (This is 
+     * strictly speaking not correct since applications can live
+     * after their exceptions stop a thread.)
+     * Exit with a value < 0 if there were exceptions in loading
+     * the target class.  Messages are printed to OUT_STREAM.
+     */
+    public static void main(String[] args) {
+        String classname = "no property : " + PROP_NAME;
+        Method main = null;
+        // setup: this try block is for loading main method - return -1 if fail
+        try {
+            // access classname from jvm arg 
+            classname = System.getProperty(PROP_NAME);
+            // this will fail if the class is not available from this classloader
+            Class cl = Class.forName(classname);
+            final Class[] argTypes = new Class[] {String[].class};
+            // will fail if no main method
+            main = cl.getMethod("main", argTypes);
+            if (!Modifier.isStatic(main.getModifiers())) {
+                PrintStream outStream = OUT_STREAM;
+                if (null != outStream) outStream.println("main is not static");
+                result = -1;
+            }
+            // if user also request loading of all classes...
+            String classesDir = System.getProperty(CLASSDIR_NAME);
+            if ((null != classesDir) && (0 < classesDir.length())) {
+                MainWrapper.loadAllClasses(new File(classesDir));
+            }
+        } catch (Throwable t) {
+            if (1 != result) result--;
+            reportException("setup Throwable invoking class " + classname, t);
+        }
+        // run: this try block is for running things - get Throwable from our thread here
+        if ((null != main) && (0 == result)) {
+            try {
+                runInOurThreadGroup(main, args);
+            } catch (Throwable t) {
+                if (result > -1) {
+                    result++;
+                }
+                reportException("run Throwable invoking class " + classname, t);
+            }
+        }
+        if ((0 != result) && (SIGNAL_EXCEPTION)) {
+            System.exit(result);
+        }
+    }
+
+    static void runInOurThreadGroup(final Method main, final String[] args) {
+        final String classname = main.getDeclaringClass().getName();
+        ThreadGroup ourGroup = new ThreadGroup("MainWrapper ThreadGroup") {
+                public void uncaughtException(Thread t, Throwable e) {
+                    reportException("uncaughtException invoking " + classname, e);
+                    result++;
+                    if (FAIL_ON_EXCEPTION) {
+                        System.exit((SIGNAL_EXCEPTION ? result : 0));
+                    }
+                }
+            };
+        Runnable runner = new Runnable() {
+                public void run() {
+                    try {
+                        main.invoke(null, new Object[] {args});
+                    } catch (InvocationTargetException e) {
+                        result = -1;
+                        reportException("InvocationTargetException invoking " + classname, e);
+                    } catch (IllegalAccessException e) {
+                        result = -1;
+                        reportException("IllegalAccessException invoking " + classname, e);
+                    }
+                }
+            };
+        Thread newMain = new Thread(ourGroup, runner, "pseudo-main");
+        newMain.start();
+        try {
+            newMain.join();
+        } catch (InterruptedException e) {
+            result = -1; // todo: InterruptedException might be benign - retry?
+            reportException("Interrupted while waiting for to join " + newMain, e);
+        }
+    }
+
+    /** 
+     * Try to load all classes in a directory.
+     * @throws Error if any failed
+     */
+    static protected void loadAllClasses(File classesDir) {
+        if (null != classesDir) {
+            String[] names = LangUtil.classesIn(classesDir);
+            StringBuffer err = new StringBuffer();
+            LangUtil.loadClasses(names, null, err);
+            if (0 < err.length()) {
+                throw new Error("MainWrapper Errors loading classes: " 
+                                 + err.toString());
+            }
+        }
+    }
+
+    static void reportException(String context, Throwable t) {
+        PrintStream outStream = OUT_STREAM;
+        if (null != outStream) {
+            while ((null != t) && 
+                   (InvocationTargetException.class.isAssignableFrom(t.getClass()))) {
+                t = ((InvocationTargetException) t).getTargetException();
+            }
+            outStream.println("  context: " + context);
+            outStream.println("  message: " + t.getMessage());
+            t.printStackTrace(outStream);
+        }
+    }
+
+} // MainWrapper
diff --git a/testing/src/org/aspectj/testing/gui/resources/gui.properties b/testing/src/org/aspectj/testing/gui/resources/gui.properties
new file mode 100644 (file)
index 0000000..56a5878
--- /dev/null
@@ -0,0 +1,53 @@
+gui.actions=quit delete reload open save saveas stop start
+
+
+quit.class=org.aspectj.testing.gui.Gui$QuitAction
+quit.icon=Quit24.gif
+quit.tooltip=Quit
+
+delete.class=org.aspectj.testing.gui.Gui$DeleteAction
+delete.icon=Delete24.gif
+delete.tooltip=Delete Test Cases
+
+stop.class=org.aspectj.testing.gui.Gui$StopAction
+stop.icon=Stop24.gif
+stop.tooltip=Stop running test
+
+reload.class=org.aspectj.testing.gui.Gui$ReloadAction
+reload.icon=Reload24.gif
+reload.tooltip=Reload current test
+
+open.class=org.aspectj.testing.gui.Gui$OpenAction
+open.icon=Open24.gif
+open.tooltip=Open a test
+
+save.class=org.aspectj.testing.gui.Gui$SaveAction
+save.icon=Save24.gif
+save.tooltip=Save current test
+
+saveas.class=org.aspectj.testing.gui.Gui$SaveAsAction
+saveas.icon=SaveAs24.gif
+saveas.tooltip=Save current test as...
+
+start.class=org.aspectj.testing.gui.Gui$StartAction
+start.icon=Start24.gif
+start.tooltip=Start test
+
+editor.actions=openfile savefile savefileas
+
+openfile.class=org.aspectj.testing.gui.Gui$OpenAction
+openfile.icon=Open24.gif
+openfile.tooltip=Open a file
+
+savefile.class=org.aspectj.testing.gui.Gui$SaveAction
+savefile.icon=Save24.gif
+savefile.tooltip=Save currect file
+
+savefileas.class=org.aspectj.testing.gui.Gui$SaveAsAction
+savefileas.icon=SaveAs24.gif
+savefileas.tooltip=Save current file as...
+
+gui.width=700
+gui.less.height=100
+gui.top.divider.location=0.70
+gui.center.divider.location=0.60
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Back16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Back16.gif
new file mode 100644 (file)
index 0000000..f48362d
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Back16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Delete24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Delete24.gif
new file mode 100644 (file)
index 0000000..d8c8c2d
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Delete24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Forward16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Forward16.gif
new file mode 100644 (file)
index 0000000..d25a3f9
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Forward16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Open16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Open16.gif
new file mode 100644 (file)
index 0000000..e3ea2f5
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Open16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Open24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Open24.gif
new file mode 100644 (file)
index 0000000..cbb2e08
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Open24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Quit24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Quit24.gif
new file mode 100644 (file)
index 0000000..90515c8
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Quit24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Reload24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Reload24.gif
new file mode 100644 (file)
index 0000000..3ead627
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Reload24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Save16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Save16.gif
new file mode 100644 (file)
index 0000000..954f1ac
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Save16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Save24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Save24.gif
new file mode 100644 (file)
index 0000000..8813f46
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Save24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/SaveAs16.gif b/testing/src/org/aspectj/testing/gui/resources/images/SaveAs16.gif
new file mode 100644 (file)
index 0000000..8d3929c
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/SaveAs16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/SaveAs24.gif b/testing/src/org/aspectj/testing/gui/resources/images/SaveAs24.gif
new file mode 100644 (file)
index 0000000..40a3d87
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/SaveAs24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Start16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Start16.gif
new file mode 100644 (file)
index 0000000..a72a9ca
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Start16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Start24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Start24.gif
new file mode 100644 (file)
index 0000000..63230a9
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Start24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Stop16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Stop16.gif
new file mode 100644 (file)
index 0000000..4918452
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Stop16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Stop24.gif b/testing/src/org/aspectj/testing/gui/resources/images/Stop24.gif
new file mode 100644 (file)
index 0000000..3fae382
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Stop24.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Test16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Test16.gif
new file mode 100644 (file)
index 0000000..b728bcb
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Test16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Testfailed16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Testfailed16.gif
new file mode 100644 (file)
index 0000000..44b9ea6
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Testfailed16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Testopen16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Testopen16.gif
new file mode 100644 (file)
index 0000000..cd090c8
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Testopen16.gif differ
diff --git a/testing/src/org/aspectj/testing/gui/resources/images/Testpassed16.gif b/testing/src/org/aspectj/testing/gui/resources/images/Testpassed16.gif
new file mode 100644 (file)
index 0000000..c5689e6
Binary files /dev/null and b/testing/src/org/aspectj/testing/gui/resources/images/Testpassed16.gif differ
diff --git a/testing/src/org/aspectj/testing/harness/bridge/AbstractRunSpec.java b/testing/src/org/aspectj/testing/harness/bridge/AbstractRunSpec.java
new file mode 100644 (file)
index 0000000..944f096
--- /dev/null
@@ -0,0 +1,883 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.util.BridgeUtil;
+import org.aspectj.testing.xml.IXmlWritable;
+import org.aspectj.testing.xml.SoftMessage;
+import org.aspectj.testing.xml.XMLWriter;
+import org.aspectj.util.LangUtil;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Base class for initialization of components expecting messages,
+ * options, files/paths, and source locations (resolved files),
+ * and potentially containing child Spec.
+ * <p>
+ * <u>initialization</u>: This defines bean/xml setters for all.
+ * This converts String to IMessage using
+ * {@link MessageUtil#readMessage(String)}
+ * and String to ISourceLocation using 
+ * {@link BridgeUtil#makeSourceLocation(input)}.
+ * See those APIs for input form and limitations.
+ * A Spec also accepts (or rejects) runtime configuration from a parent
+ * in {@link adoptParentValues(RT, IMessageHandler)}.  
+ * Since some children Spec may balk but this parent Spec continue,
+ * use {@link getChildren()} to get the full list of children Spec,
+ * but {@link getWorkingChildren()} to get the list of children that
+ * are not being skipped in accordance with runtime configuration.
+ * <p> 
+ * <u>subclassing</u>: subclasses wishing other than
+ * the default behavior for reading String input should override the
+ * corresponding (bean) setter.  They can also override the 
+ * add{foo} methods to get notice or modify objects constructed
+ * from the input.
+ * <p>
+ * <u>bean properties</u>: because this is designed to work
+ * by standard Java bean introspection, take care to follow
+ * bean rules when adding methods.   In particular, a property
+ * is illegal if the setter takes a different type than the
+ * getter returns.  That means, e.g., that all List and array[] getters
+ * should be named "get{property}[List|Array]".  Otherwise 
+ * the XML readers will silently fail to set the property
+ * (perhaps with trace information that the property had
+ * no write method or was read-only).
+ * <p>
+ * <u>Coordination with writers</u>: because this reads the contents
+ * of values written by IXmlWritable, they should ensure their
+ * values are readable.  When flattening and unflattening
+ * lists, the convention is to use the {un}flattenList(..) methods
+ * in XMLWriter.
+ * @see XMLWriter@unflattenList(String)
+ * @see XMLWriter@flattenList(List)
+ */
+abstract public class AbstractRunSpec implements IRunSpec { // XXX use MessageHandler?
+
+    /** true if we expect to use a staging directory */
+    boolean isStaging;     
+    
+    protected String description;
+    
+    /** optional source location of the specification itself */
+    protected ISourceLocation sourceLocation;
+    
+    private BitSet skipSet;
+    private boolean skipAll;
+    
+    protected final String xmlElementName;
+    protected final ArrayList /*String*/ keywords; 
+    protected final ArrayList /*IMessage*/ expectedMessages;
+    protected final ArrayList /*String*/ options;
+    protected final ArrayList /*String*/ paths;
+    protected final ArrayList /*ISourceLocation*/ sourceLocations; // XXX remove?
+    protected final ArrayList /*IRunSpec*/ children;
+    protected final ArrayList /*DirChanges.Spec*/ dirChanges;
+    protected XMLNames xmlNames;
+    protected String comment;
+    
+    /** These options are 1:1 with spec, but set at runtime (not saved) */
+    public final RT runtime;
+
+    /** if true, then any child skip causes this to skip */
+       protected final boolean skipIfAnyChildSkipped;
+
+    public AbstractRunSpec(String xmlElementName) {
+        this(xmlElementName, true);
+    }
+    
+    public AbstractRunSpec(String xmlElementName, boolean skipIfAnyChildSkipped) {
+        if (null == xmlElementName) {
+            xmlElementName = "spec";
+        }
+        this.xmlElementName = xmlElementName;
+        expectedMessages = new ArrayList();
+        options = new ArrayList();
+        paths = new ArrayList();
+        sourceLocations = new ArrayList();
+        keywords = new ArrayList();
+        children = new ArrayList();
+        dirChanges = new ArrayList();
+        xmlNames = XMLNames.DEFAULT;
+        runtime = new RT();
+        this.skipIfAnyChildSkipped = skipIfAnyChildSkipped;
+    }    
+    
+
+    /** @param comment ignored if null */
+    public void setComment(String comment) {
+        if (!LangUtil.isEmpty(comment)) {
+            this.comment = comment;
+        }
+    }
+    
+    public void setStaging(boolean staging) {
+        isStaging = staging;
+    }
+    
+    boolean isStaging() {
+        return isStaging;
+    }
+        
+    // ------- description (title, label...)    
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+     
+    // ------- source location of the spec
+    
+    public void setSourceLocation(ISourceLocation sourceLocation) {
+        this.sourceLocation = sourceLocation;
+    }
+
+    public ISourceLocation getSourceLocation() {
+        return sourceLocation;
+    }
+     
+    // ------- keywords
+    /** @param keyword added after trimming if not empty */
+    public void setKeyword(String keyword) {
+        addKeyword(keyword);
+    }
+    
+    /** Add keyword if non-empty and not duplicate */
+    public void addKeyword(String keyword) {
+        if (!LangUtil.isEmptyTrimmed(keyword)) {
+            keyword = keyword.trim();
+            if (!keywords.contains(keyword)) {
+                keywords.add(keyword);
+            }
+        }
+    }
+    public void setKeywords(String items) {
+        addKeywords(items);
+    }
+    
+    public void addKeywords(String items) {
+        if (null != items) {
+            addKeywords(XMLWriter.unflattenList(items));
+        }    
+    }
+    public void addKeywords(String[] ra) {
+        if (null != ra) {
+            for (int i = 0; i < ra.length; i++) {
+                addKeyword(ra[i]);
+            }
+        }    
+    }
+    public ArrayList getKeywordsList() {
+        return makeList(keywords);
+    }
+
+   // ------- options - String args
+
+    /** @return ArrayList of String options */    
+    public ArrayList getOptionsList() {
+        return makeList(options);
+    }
+    
+    /** @return String[] of options */    
+    public String[] getOptionsArray() {
+        return (String[]) options.toArray(new String[0]);
+    }
+    
+    public void setOption(String option) {
+        addOption(option);
+    }
+    
+    public void addOption(String option) {
+        if ((null != option) && (0 < option.length())) {
+            options.add(option);
+        }    
+    }
+    
+    /** add options (from XML/bean) - removes any existing options */    
+    public void setOptions(String items) {
+        this.options.clear();
+        addOptions(items);
+    }
+    
+    /** 
+     * Set options, removing any existing options.
+     * @param options String[] options to use - may be null or empty
+     */
+    public void setOptionsArray(String[] options) {
+        this.options.clear();
+        if (!LangUtil.isEmpty(options)) {
+            this.options.addAll(Arrays.asList(options));
+        }
+    }
+    
+    public void addOptions(String items) {
+        if (null != items) {
+            addOptions(XMLWriter.unflattenList(items));
+        }    
+    }
+    
+    public void addOptions(String[] ra) {
+        if (null != ra) {
+            for (int i = 0; i < ra.length; i++) {
+                addOption(ra[i]);
+            }
+        }    
+    }
+
+    // -------------- source locations
+//    /** @return ArrayList of ISourceLocation sourceLocs */    
+//    public ArrayList getSourceLocationsList() {
+//        return makeList(sourceLocations);
+//    }
+//
+//    /** @return ISourceLocation[] sourceLocs */    
+//    public ISourceLocation[] getSourceLocationsArray() {
+//        return (ISourceLocation[]) sourceLocations.toArray(new ISourceLocation[0]);
+//    }
+//
+//    
+//    public void setSourceLocation(String input) {
+//        if (null != input) {
+//            ISourceLocation sl = BridgeUtil.makeSourceLocation(input);
+//            if (null != sl) {
+//                addSourceLocation(sl);
+//            }
+//            // XXX need error-handling for bad input            
+//        }
+//    }
+//    
+//    public void addSourceLocation(ISourceLocation sourceLoc) {
+//        if (null != sourceLoc) {
+//            sourceLocations.add(sourceLoc);
+//        }    
+//    }
+
+    // --------------- (String) paths
+    /** @return ArrayList of String paths */    
+    public ArrayList getPathsList() {
+        return makeList(paths);
+    }
+    
+    /** @return String[] of paths */    
+    public String[] getPathsArray() {
+        return (String[]) paths.toArray(new String[0]);
+    }
+
+//    /** @return String[] of paths, removing any matching stripPrefix prefix */    
+//    public String[] getPathsArray(String stripPrefix) {
+//        String[] result = getPathsArray();
+//        if (!LangUtil.isEmpty(stripPrefix)) {
+//            final int LEN = stripPrefix.length();
+//            for (int i = 0; i < result.length; i++) {
+//                if ((null != result[i]) && result[i].startsWith(stripPrefix)) {
+//                    result[i] = result[i].substring(LEN);
+//                }               
+//            }
+//        }
+//        return result;
+//    }
+//
+//    /** @return ArrayList of File baseDir/{path} */    
+//    public ArrayList getPathsAsFile(File baseDir) {
+//        if (null == baseDir) {
+//            baseDir = new File(".");
+//        }
+//        ArrayList result = makeList(null);
+//        for (Iterator iter = paths.iterator(); iter.hasNext();) {
+//            String path = (String) iter.next();
+//            result.add(new File(baseDir, path));
+//        }
+//        return result;
+//    }
+    
+    public void setPath(String path) {
+        addPath(path);
+    }
+
+    public void setPaths(String paths) {
+        addPaths(paths);
+    }
+    
+    public void addPath(String path) {
+        if (null != path) {
+            paths.add(path);
+        }    
+    }
+
+    public void addPaths(String items) {
+        if (null != items) {
+            addPaths(XMLWriter.unflattenList(items));
+        }    
+    }
+
+    public void addPaths(String[] ra) {
+        if (null != ra) {
+            for (int i = 0; i < ra.length; i++) {
+                addPath(ra[i]);
+            }
+        }    
+    }
+
+    public void addWrapFile(WrapFile file) {
+        if (null != file) {
+            paths.add(file.path);
+        }    
+    }
+    
+    // --------------------- dir changes
+    public void addDirChanges(DirChanges.Spec dirChangesSpec) {
+        if (null != dirChangesSpec) {
+            dirChanges.add(dirChangesSpec);
+        }
+    }
+    
+    // --------------------- messages
+    public void setMessage(String message) {
+        addMessage(message);
+    }
+
+    public void addMessage(IMessage message) {
+        if (null != message) {
+            expectedMessages.add(message);
+        }
+    }
+    
+    public void addMessage(String message) {
+        if (null != message) {
+            IMessage m = BridgeUtil.readMessage(message);
+            expectedMessages.add(m);
+        }
+    }
+    
+    /** this can ONLY work if each item has no internal comma 
+     */
+    public void addMessages(String items) {
+        if (null != items) {
+            String[] ra = XMLWriter.unflattenList(items);
+            for (int i = 0; i < ra.length; i++) {
+                addMessage(ra[i]);
+            }
+        }    
+    }
+    public void addMessages(List messages) {
+        if (null != messages) {
+            for (Iterator iter = messages.iterator(); iter.hasNext();) {
+                               Object o = iter.next();
+                               if (o instanceof IMessage) {
+                    addMessage((IMessage) o);
+                } else {
+                    // XXX warning?
+                }
+                       }
+        }    
+    }
+
+    public ArrayList getMessages(IMessage.Kind kind) {
+        return makeList(MessageUtil.getMessages(expectedMessages, kind));
+    }
+    
+
+    /** @return int number of message of this kind (optionally or greater */
+    public int numMessages(IMessage.Kind kind, boolean orGreater) {
+        return MessageUtil.numMessages(expectedMessages, kind, orGreater);
+    }
+    
+    public ArrayList getMessages() {
+        return getMessages(null); // XXX special meaning of null
+    }
+    
+    
+    public void addChild(IRunSpec child) {
+        // fyi, child is added when complete (depth-first), not when initialized,
+        // so cannot affect initialization of child here
+        if (null != child) {
+            children.add(child); 
+        }
+    }
+    
+    /** @return copy of children list */
+    public ArrayList getChildren() {
+        return makeList(children);
+    }
+
+    /** @return copy of children list without children to skip */
+    public ArrayList getWorkingChildren() {
+        if (skipAll) {
+            return new ArrayList();
+        }
+        if (null == skipSet) {
+            return getChildren();
+        }
+        ArrayList result = new ArrayList();
+        int i = 0;
+        for (Iterator iter = children.listIterator(); 
+            iter.hasNext(); i++) {
+            Object child = iter.next();
+                   if (!skipSet.get(i)) {
+                result.add(child);
+            }
+               }
+        return result;
+    }
+
+    /** 
+     * Recursively absorb parent values if different.
+     * This implementation calls doAdoptParentValues(..)
+     * and then calls this for any children.  
+     * This is when skipped children are determined.
+     * Children may elect to balk at this point, reducing the
+     * number of children or causing this spec to skip if
+     * skipIfAnyChildrenSkipped.  For each test skipped, either
+     * this doAdoptParentValues(..) or the child's adoptParentValues(..)
+     * should add one info message with the reason this is being skipped.
+     * The only reason to override this would be to NOT
+     * invoke the same for children, or to do something similar
+     * for children which are not AbstractRunSpec.
+     * @param parentRuntime the RT values to adopt - ignored if null
+     * @param handler the IMessageHandler for info messages when skipping
+     * @return false if this wants to be skipped, true otherwise
+     */
+    public  boolean adoptParentValues(RT parentRuntime, IMessageHandler handler) {
+        boolean skipped = false;
+        skipAll = false;
+        skipSet = new BitSet();
+        if (null != parentRuntime) {
+            skipped = !doAdoptParentValues(parentRuntime, handler);
+            if (skipped && skipIfAnyChildSkipped) { // no need to continue checking
+                skipAll = true;
+                return false;
+            }
+            int i = 0;
+            for (ListIterator iter = children.listIterator(); iter.hasNext(); i++) {
+                               IRunSpec child = (IRunSpec) iter.next();
+                               if (child instanceof AbstractRunSpec) { // XXX ugly instanceof
+                    AbstractRunSpec arsChild = (AbstractRunSpec) child;
+                    if (!arsChild.adoptParentValues(runtime, handler)) {
+                        skipSet.set(i);
+                        //iter.remove();
+                        if (!skipped) {
+                            skipped = true;
+                            if (skipIfAnyChildSkipped) { // no need to continue checking
+//                                String m = "skipping " + toString() + " because child " 
+//                                    + arsChild + " skipped";
+//                                MessageUtil.info(handler, m);
+                                skipAll = true;
+                                //children.clear();
+                                return false;
+                            }
+                        }
+                    }
+                }
+                       }
+        }
+        return true;
+    }
+    
+    /** 
+     * Adopt parent values.
+     * This implementation makes a local copy.
+     * If we interpret (and absorb) any options, they should be removed
+     * from parentRuntime.
+     * This sets verbose if different (override)
+     * and directly adopts parentOptions if ours is null
+     * and otherwise adds any non-null options we don't already have.
+     * setting verbose and adding to parent options.
+     * Implementors  override this to affect how parent values are adopted. 
+     * Implementors should not recurse into children.
+     * This method may be called multiple times, so implementors 
+     * should not destroy any spec information.
+     * Always add an info message when returning false to skip
+     * @param parentRuntime the RT values to adopt - never null
+     * @return false if this wants to be skipped, true otherwise
+     */
+    protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
+        if (runtime.verbose != parentRuntime.verbose) {
+            runtime.verbose = parentRuntime.verbose;
+        }
+        if (!LangUtil.isEmpty(runtime.parentOptions)) {
+            runtime.parentOptions.clear();
+        }
+        if (!LangUtil.isEmpty(parentRuntime.parentOptions)) {
+            runtime.parentOptions.addAll(parentRuntime.parentOptions);
+        }
+        return true;
+    }  
+    
+    /** 
+     * Implementations call this when signalling skips to ensure consistency
+     * in message formatting
+     * @param handler the IMessageHandler sink - not null
+     * @param reason the String reason to skip - not null
+     */
+    protected void skipMessage(IMessageHandler handler, String reason) {
+        LangUtil.throwIaxIfNull(handler, "handler");
+        LangUtil.throwIaxIfNull(handler, "reason");
+        // XXX for Runs, label does not identify the test 
+        String label = toString();
+        MessageUtil.info(handler, "skipping \"" + label + "\" because " + reason);
+    }
+    
+    // --------------------------- writing xml - would prefer castor..
+    
+    /**
+     * Control XML output by renaming or suppressing output for
+     * attributes and subelements.
+     * Subelements are skipped by setting the XMLNames booleans to false.
+     * Attributes are skipped by setting their name to null.
+     * @param names XMLNames with new names and/or suppress flags.
+     */
+    protected void setXMLNames(XMLNames names) {
+        if (null != names) {
+            xmlNames = names;
+        }
+    }
+    
+    /** @return null if value is null or name="{value}" otherwise */
+    private String makeAttr(XMLWriter out, String name, String value) {
+        if (null == value) {
+            return null;
+        }
+        return out.makeAttribute(name, value);
+    }
+    
+    /** @return null if list is null or empty or name="{flattenedList}" otherwise */
+    private String makeAttr(XMLWriter out, String name, List list) {
+        if (LangUtil.isEmpty(list)) {
+            return null;
+        }
+        String flat = out.flattenList(list);
+        return out.makeAttribute(name, flat);
+    }
+    
+    /** @return true if writeAttributes(..) will produce any output */
+    protected boolean haveAttributes() {
+        return ((!LangUtil.isEmpty(xmlNames.descriptionName)
+                    && !LangUtil.isEmpty(description))
+               || (!LangUtil.isEmpty(xmlNames.keywordsName)
+                    && !LangUtil.isEmpty(keywords))                    
+               || (!LangUtil.isEmpty(xmlNames.optionsName)
+                    && !LangUtil.isEmpty(options)) 
+               || (!LangUtil.isEmpty(xmlNames.pathsName)
+                    && !LangUtil.isEmpty(paths)));
+    }
+    
+    /**
+     * Write attributes without opening or closing elements/attributes.
+     * An attribute is written only if the value is not empty
+     * and the name in xmlNames is not empty
+     */
+    protected void writeAttributes(XMLWriter out) {
+        if (!LangUtil.isEmpty(xmlNames.descriptionName)
+            && !LangUtil.isEmpty(description)) {
+            out.printAttribute(xmlNames.descriptionName, description);
+        }
+        if (!LangUtil.isEmpty(xmlNames.keywordsName)
+            && !LangUtil.isEmpty(keywords)) {
+            out.printAttribute(xmlNames.keywordsName, out.flattenList(keywords));
+        }
+        if (!LangUtil.isEmpty(xmlNames.optionsName)
+            && !LangUtil.isEmpty(options)) {
+            out.printAttribute(xmlNames.optionsName, out.flattenList(options));
+        }
+        if (!LangUtil.isEmpty(xmlNames.pathsName)
+            && !LangUtil.isEmpty(paths)) {
+            out.printAttribute(xmlNames.pathsName, out.flattenList(paths));
+        }
+        if (!LangUtil.isEmpty(xmlNames.commentName)
+            && !LangUtil.isEmpty(comment)) {
+            out.printAttribute(xmlNames.commentName, comment);
+        }
+    }
+
+    /**
+     * The default implementation writes everything as attributes,
+     * then subelements for dirChanges, messages, then subelements for children.
+     * Subclasses that override may delegate back for any of these.
+     * Subclasses may also set XMLNames to name or suppress any attribute
+     * or subelement.
+     * @see writeMessages(XMLWriter)
+     * @see writeChildren(XMLWriter)
+     * @see IXmlWritable#writeXml(XMLWriter) 
+     */
+    public void writeXml(XMLWriter out) {
+        out.startElement(xmlElementName,false);
+        writeAttributes(out);
+        out.endAttributes();
+        if (!xmlNames.skipMessages) {
+            writeMessages(out);
+        }
+        if (!xmlNames.skipChildren) {
+            writeChildren(out);            
+        }
+        out.endElement(xmlElementName);
+    }
+
+       /**
+        * Write messages.  Assumes attributes are closed,
+     * can write child elements of current element.
+        */
+       protected void writeMessages(XMLWriter out) {
+        if (0 < expectedMessages.size()) {
+            SoftMessage.writeXml(out, expectedMessages);
+        }
+
+       }
+
+    /**
+     * Write children.  Assumes attributes are closed,
+     * can write child elements of current element.
+     */
+    protected void writeChildren(XMLWriter out) {
+        if (0 < children.size()) {
+            for (Iterator iter = children.iterator(); iter.hasNext();) {
+                IXmlWritable self = (IXmlWritable) iter.next();
+                self.writeXml(out);
+            }
+        }
+    }
+
+    // --------------------------- logging
+    
+    public void printAll(PrintStream out, String prefix) {
+        out.println(prefix + toString());
+        for (Iterator iter = children.iterator(); iter.hasNext();) {
+                       AbstractRunSpec child = (AbstractRunSpec) iter.next(); // IRunSpec
+            child.printAll(out, prefix + "    ");                      
+               }
+    }
+    
+    /**
+     * default implementation returns the description if not empty
+     * or the unqualified class name otherwise.
+     * Subclasses should not call toString from here unless they reimplement it.
+     * @return name of this thing or type 
+     */
+    protected String getPrintName() {
+        if (!LangUtil.isEmpty(description)) {
+            return description;
+        } else {
+            return LangUtil.unqualifiedClassName(this);
+        }
+    }
+    
+    /** @return summary count of spec elements */
+    public String toString() {        
+        return getPrintName() + "(" + containedSummary() + ")"; 
+    }
+    
+    /** @return String of the form (# [options|paths|locations|messages]).. */
+    protected String containedSummary() {
+        int nOptions = options.size();
+        int nPaths = paths.size();
+        int nLoc = sourceLocations.size();
+        int nMssg = expectedMessages.size();
+        return 
+            ( (nOptions == 0 ? "" : nOptions + " options " )
+            + (nPaths == 0 ? "" : nPaths + " paths " )
+            + (nLoc == 0 ? "" : nLoc + " locations " )
+            + (nMssg == 0 ? "" : nMssg + " messages" )).trim();
+    }
+    
+    public String toLongString() { 
+        String mssg = "";
+        if (expectedMessages.size() > 0) {
+            // XXX add MessageHandler.renderCounts(IMessage[] messages)
+            MessageHandler h = new MessageHandler(); 
+            for (Iterator iter = expectedMessages.iterator(); iter.hasNext();) {
+                               h.handleMessage((IMessage) iter.next());
+                       }
+            mssg = " expected messages (" + MessageUtil.renderCounts(h) + ")";
+        }
+        return getPrintName() + containedSummary() + mssg.trim();
+    }
+    
+    private ArrayList makeList(List list) {
+        ArrayList result = new ArrayList();
+        if (null != list) {
+            result.addAll(list);
+        }
+        return result;
+    }
+    
+    /** 
+     * Subclasses use this to rename attributes or omit attributes or subelements.
+     * To suppress output of an attribute, * pass "" as the name of the attribute. 
+     * To use default entries, pass null for that entry.
+     */
+    public static class XMLNames {
+        public static final XMLNames DEFAULT =
+            new XMLNames(null, "description", "sourceLocation", 
+                    "keywords", "options", "paths", "comment", false, false, false);
+        final String descriptionName;
+        final String sourceLocationName;
+        final String keywordsName;
+        final String optionsName;
+        final String pathsName;
+        final String commentName;
+        final boolean skipDirChanges;
+        final boolean skipMessages;
+        final boolean skipChildren;
+        /** reset all names/behavior or pass defaultNames 
+         * as the defaults for any null elements 
+         */
+        XMLNames(XMLNames defaultNames,
+                String descriptionName,
+                String sourceLocationName,
+                String keywordsName,
+                String optionsName,
+                String pathsName,
+                String commentName,
+                boolean skipDirChanges,
+                boolean skipMessages,
+                boolean skipChildren) {
+            this.skipDirChanges = skipDirChanges;
+            this.skipMessages = skipMessages;
+            this.skipChildren = skipChildren;
+            if (null != defaultNames) {
+                this.descriptionName = (null != descriptionName? descriptionName : defaultNames.descriptionName);
+                this.sourceLocationName = (null != sourceLocationName ? sourceLocationName : defaultNames.sourceLocationName);
+                this.keywordsName = (null != keywordsName ? keywordsName : defaultNames.keywordsName);
+                this.optionsName = (null != optionsName ? optionsName : defaultNames.optionsName);
+                this.pathsName = (null != pathsName ? pathsName : defaultNames.pathsName);
+                this.commentName = (null != commentName ? commentName : defaultNames.commentName);
+            } else {
+                this.descriptionName = descriptionName;
+                this.sourceLocationName = sourceLocationName;
+                this.keywordsName = keywordsName;
+                this.optionsName = optionsName;
+                this.pathsName = pathsName;
+                this.commentName = commentName;
+            }
+        }
+    }
+
+    
+    /** subclasses implement this to create and set up a run */
+    abstract public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator);
+
+    /** This is for separate file (sub-) elements with path attributes */
+    public static class WrapFile {
+        public String path;
+        public void setPath(String path) {
+            this.path = path;
+        }
+    }
+    
+    /** segregate runtime-only state in spec */
+    public static class RT {        
+        /** true if we should emit verbose messages */
+        private boolean verbose;
+        
+        /** null unless parent set options for children to consider */
+        final private ArrayList /*String*/ parentOptions;
+        
+        public RT() {
+            parentOptions = new ArrayList();
+        }
+        
+        public boolean isVerbose() {
+            return verbose;
+        }
+        
+        /** 
+         * Set parent options - old options destroyed.
+         * Will result in duplicates if duplicates added.
+         * Null or empty entries are ignored
+         * @param options ignored if null or empty
+         */
+        public void setOptions(String[] options) {
+            parentOptions.clear();
+            if (!LangUtil.isEmpty(options)) {
+                for (int i = 0; i < options.length; i++) {
+                    if (!LangUtil.isEmpty(options[i])) {
+                        parentOptions.add(options[i]);
+                    }
+                               }
+            }
+        }
+        
+        /** 
+         * Copy values from another RT 
+         * @param toCopy the RT to copy from 
+         * @throws IllegalArgumentException if toCopy is null
+         */
+        public void copy(RT toCopy) {
+            LangUtil.throwIaxIfNull(toCopy, "parent");
+            parentOptions.clear();
+            parentOptions.addAll(toCopy.parentOptions);
+            verbose = toCopy.verbose;
+        }
+        
+        /** 
+         * Return any parent option which has one of validOptions as a prefix,
+         * optionally absorbing (removing) the parent option.
+         * @param validOptions String[] of options to extract
+         * @param absorb if true, then remove any parent option matched
+         * @return String[] containing any validOptions[i] in parentOptions
+         *          (at most once)
+         */
+        public String[] extractOptions(String[] validOptions, boolean absorb) {
+            if (LangUtil.isEmpty(validOptions) || LangUtil.isEmpty(parentOptions)) {
+                return new String[0];
+            }
+            ArrayList result = new ArrayList();
+            boolean haveOption = false;
+            for (int i = 0; i < validOptions.length; i++) {
+                               String option = validOptions[i];
+                if (LangUtil.isEmpty(option)) {
+                    continue;
+                }
+                for (ListIterator iter = parentOptions.listIterator(); iter.hasNext();) {
+                                       String parentOption = (String) iter.next();
+                                       if (parentOption.startsWith(option)) {
+                        result.add(parentOption);
+                        if (absorb) {
+                            iter.remove();
+                        }
+                    }
+                               }
+                       }
+            return (String[]) result.toArray(new String[0]);
+        }
+        
+        /** Get ListIterator that permits removals */
+        ListIterator getListIterator() {
+            return parentOptions.listIterator();
+        }
+        
+        /**
+                * Enable verbose logging
+                * @param verbose if true, do verbose logging
+         */
+               public void setVerbose(boolean verbose) {
+            if (this.verbose != verbose) {
+                this.verbose = verbose;
+            }
+               }
+    }   // class RT
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java b/testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java
new file mode 100644 (file)
index 0000000..943110d
--- /dev/null
@@ -0,0 +1,282 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.util.BridgeUtil;
+import org.aspectj.testing.util.Diffs;
+import org.aspectj.util.LangUtil;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Adapter to pass messages through to handler,
+ * suppress output of expected errors and warnings,
+ * and calculate differences between expected and
+ * actual messages.
+ */
+public class AjcMessageHandler extends MessageHandler {     
+
+    /** Comparator for enclosed IMessage diffs, etc. */
+    public static final Comparator COMP_IMessage
+        = BridgeUtil.Comparators.WEAK_IMessage;
+        
+    /** Comparator for enclosed File diffs, etc. */
+    public static final Comparator COMP_File
+        = BridgeUtil.Comparators.WEAK_File;
+
+
+    /** unmodifiable list of IMessage messages of type IMesssage.ERROR */
+    final List expectedErrors; // revert to IMessageHolder for generality?
+    
+    /** unmodifiable list of IMessage messages of type IMesssage.WARNING */
+    final List expectedWarnings;
+
+    /** unmodifiable list of File expected to be recompiled */
+    final List expectedRecompiled;
+    
+    /** list of File actually recompiled */
+    List actualRecompiled;
+    
+    /** if true, ignore warnings when calculating diffs and passed() */
+    boolean ignoreWarnings;
+    
+    /** cache expected/actual diffs, nullify if any new message */
+    transient CompilerDiffs diffs;
+
+       private boolean expectingCommandTrue;
+    
+    /** 
+     * @param messages List of IMessage to extract ERROR and WARNING from 
+     */
+    AjcMessageHandler(List messages) {
+        this(MessageUtil.getMessages(messages, IMessage.ERROR),
+            MessageUtil.getMessages(messages, IMessage.WARNING));
+    }
+
+   /** 
+     * @param errors unmodifiable List of IMessage of kind ERROR to adopt
+     * @param warnings unmodifiable List of IMessage of kind WARNING to adopt 
+     */
+    AjcMessageHandler(List errors, List warnings) {
+        this(errors, warnings, null);
+    }
+
+    AjcMessageHandler(List errors, List warnings, List recompiled) {
+        this.expectedErrors = LangUtil.safeList(errors);
+        this.expectedWarnings = LangUtil.safeList(warnings);
+        this.expectedRecompiled = LangUtil.safeList(recompiled);
+        expectingCommandTrue = (0 == expectedErrors.size());
+    }
+    
+    public void setIgnoreWarnings(boolean ignoreWarnings) {
+        this.ignoreWarnings = ignoreWarnings;
+    }
+    
+    /** clear out any actual values to be re-run */
+    public void init() {
+        super.init();
+        actualRecompiled = null;
+        diffs = null;
+    }
+
+    /**
+     * Return true if we have this kind of
+     * message for the same line and store all messages.
+     * @see bridge.tools.impl.ErrorHandlerAdapter#doShowMessage(IMessage)
+     * @return true if message handled (processing should abort)
+     */
+    public boolean handleMessage(IMessage message) {
+        if (null == message) {
+            throw new IllegalArgumentException("null message");
+        }
+        messages.add(message);
+        IMessage.Kind kind = message.getKind();
+        return expecting(message, getExpected(kind));
+    }
+    
+    /** 
+     * Set the actual files recompiled.
+     * @param List of File recompiled - may be null; adopted but not modified
+     * @throws IllegalStateException if they have been set already.
+     */
+    public void setRecompiled(List list) {
+        if (null != actualRecompiled) {
+            throw new IllegalStateException("actual recompiled already set");
+        }
+        this.actualRecompiled = LangUtil.safeList(list);
+    }
+    
+    /** @return immutable List of IMessage expected of this kind */
+    public List getExpected(IMessage.Kind kind) {
+        if (IMessage.ERROR == kind) {
+            return expectedErrors;
+        } else if (IMessage.WARNING == kind) {
+            return expectedWarnings;
+        } else {
+            return Collections.EMPTY_LIST;
+        }
+    }
+    
+    /** 
+     * Check if the message was expected, and clear diffs if not.
+     * @return true if we expect a message of this kind with this line number 
+     */
+    protected boolean expecting(IMessage message, List sink) { // XXX ignores File
+        if ((null != message) && (0 < sink.size())) {
+            // XXX need to cache list: int[] errLines, warningLines;
+            for (Iterator iter = sink.iterator(); iter.hasNext();) {
+                       IMessage m = (IMessage) iter.next();
+                if (0 == COMP_IMessage.compare(message, m)) {
+                    return true;
+                }
+               }
+        }
+        if (null != diffs) {
+            diffs = null;
+        }
+        return false;
+    }
+
+    /** Generate differences between expected and actual errors and warnings */
+    public CompilerDiffs getCompilerDiffs() {
+        if (null == diffs) {
+            final List actualErrors = Arrays.asList(getMessages(IMessage.ERROR, IMessageHolder.EQUAL));
+            final List actualWarnings = Arrays.asList(getMessages(IMessage.WARNING, IMessageHolder.EQUAL));
+            Diffs errors = new Diffs("error", expectedErrors, actualErrors, COMP_IMessage);
+            Diffs warnings = new Diffs("warning", expectedWarnings, actualWarnings, COMP_IMessage);
+            Diffs recompiled = new Diffs("recompiled", expectedRecompiled, actualRecompiled, COMP_File);
+            diffs = new CompilerDiffs(errors, warnings, recompiled);
+        }
+        return diffs;        
+    }
+    
+    /** calculate passed based on default or set value for ignoreWarnings */
+    public boolean passed() {
+        return passed(ignoreWarnings);
+    }
+    
+    /** @return true if we are expecting the command to fail - i.e., any expected errors */
+    public boolean expectingCommandTrue() {
+        return expectingCommandTrue;
+    }
+    
+    /**
+     * Get the (current) result of this run,
+     * ignoring differences in warnings on request.
+     * Note it may return passed (true) when there are expected error messages.
+     * @return false 
+     * if there are any fail or abort messages,
+     * or if the expected errors, warnings, or recompiled do not match actual.
+     * 
+     */
+    public boolean passed(boolean ignoreWarnings) {
+        if (hasAnyMessage(IMessage.FAIL, IMessageHolder.EQUAL)) {
+            return false;
+        }   
+        
+        CompilerDiffs diffs = getCompilerDiffs();
+        if (!ignoreWarnings) {
+            return (!diffs.different);
+        } else {
+            return ((!diffs.errors.different) && (!diffs.recompiled.different));
+        }
+    }
+    
+    /**
+     * Report results to a handler,
+     * adding all messages
+     * and creating fail messages for each diff.
+     */
+    public void report(IMessageHandler handler) {
+        if (null == handler) {
+            MessageUtil.debug(this, "report got null handler");
+        }
+        IMessage[] messages = getMessages(null, IMessageHolder.EQUAL);
+        for (int i = 0; i < messages.length; i++) {
+                       handler.handleMessage(messages[i]);
+               }
+        CompilerDiffs diffs = getCompilerDiffs();
+        if (diffs.different) {
+            diffs.errors.report(handler, IMessage.FAIL);
+            diffs.warnings.report(handler, IMessage.FAIL);
+            diffs.recompiled.report(handler, IMessage.FAIL);
+        }
+    }
+    
+    /** @return String consisting of differences and any other messages */
+    public String toString() {
+        CompilerDiffs diffs = getCompilerDiffs();
+        StringBuffer sb = new StringBuffer(super.toString());
+        final String EOL = "\n";
+        sb.append(EOL);
+        render(sb, "  unexpected error ", EOL, diffs.errors.unexpected);
+        render(sb, "unexpected warning ", EOL, diffs.warnings.unexpected);
+        render(sb, "     missing error ", EOL, diffs.errors.missing);
+        render(sb, "   missing warning ", EOL, diffs.warnings.missing);
+        render(sb, "              fail ", EOL, getList(IMessage.FAIL));
+        render(sb, "             abort ", EOL, getList(IMessage.ABORT));
+        render(sb, "              info ", EOL, getList(IMessage.INFO));
+        return sb.toString(); // XXX cache toString
+    }
+    
+    /** @return immutable list of a given kind - use  null for all kinds */
+    private List getList(IMessage.Kind kind) {
+        if ((null == kind) || (0 == numMessages(kind, IMessageHolder.EQUAL))) {
+            return Collections.EMPTY_LIST;
+        }
+        return Arrays.asList(getMessages(kind, IMessageHolder.EQUAL));        
+    }
+    
+    /** @return "" if no items or {prefix}{item}{suffix}... otherwise */
+    private void render( // LangUtil instead?
+        StringBuffer result,
+        String prefix,
+        String suffix,
+        List items) {
+        if ((null != items)) {
+            for (Iterator iter = items.iterator(); iter.hasNext();) {
+                            result.append(prefix + iter.next() + suffix);
+            }
+        }
+    }
+    
+    /** compiler results for errors, warnings, and recompiled files */
+    public static class CompilerDiffs {
+        public final Diffs errors;
+        public final Diffs warnings;
+        public final Diffs recompiled;
+        public final boolean different;
+        
+        public CompilerDiffs(Diffs errors, Diffs warnings) {
+            this(errors, warnings, Diffs.NONE);
+        }
+        public CompilerDiffs(Diffs errors, Diffs warnings, Diffs recompiled) {
+            this.errors = errors;
+            this.warnings = warnings;
+            this.recompiled = recompiled;
+            different = (warnings.different || errors.different
+                         || recompiled.different);
+        }
+    }
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/AjcTest.java b/testing/src/org/aspectj/testing/harness/bridge/AjcTest.java
new file mode 100644 (file)
index 0000000..b1e9e9b
--- /dev/null
@@ -0,0 +1,371 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.Runner;
+import org.aspectj.testing.xml.XMLWriter;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An AjcTest has child subruns (compile, [inc-compile|run]*).
+ */
+public class AjcTest extends RunSpecIterator {
+    
+    /** Unwrap an AjcTest.Spec from an IRunStatus around an AjcTest */
+    public static Spec unwrapSpec(IRunStatus status) {
+        if (null != status) {
+            Object id = status.getIdentifier();
+            if (id instanceof Runner.IteratorWrapper) {
+                IRunIterator iter = ((Runner.IteratorWrapper) id).iterator;
+                if (iter instanceof AjcTest) {
+                    return (Spec) ((AjcTest) iter).spec;
+                }
+            }
+        }
+        return null;        
+    }
+    
+    /** Unwrap initial CompilerRun.Spec from an AjcTest.Spec */
+    public static CompilerRun.Spec unwrapCompilerRunSpec(Spec spec) {
+        if (null != spec) {
+            List kids = spec.getChildren();
+            if (0 < kids.size()) {
+                Object o = kids.get(0);
+                if (o instanceof CompilerRun.Spec) {
+                    return (CompilerRun.Spec) o;
+                }
+            }
+        }
+        return null;        
+    }
+
+    /** The spec creates the sandbox, so we use it throughout */
+       public AjcTest(Spec spec, Sandbox sandbox, Validator validator) {
+          super(spec, sandbox, validator, true);
+    }
+    
+    /**
+     * Clear the command from the sandbox, to avoid memory leaks.
+        * @see org.aspectj.testing.harness.bridge.RunSpecIterator#iterationCompleted()
+        */
+       public void iterationCompleted() {
+               super.iterationCompleted();
+        sandbox.clearCommand(this);
+       }
+    
+    
+    /** 
+     * Specification for an ajc test.
+     * Keyword directives are global/parent options passed as
+     * <pre>-ajctest[Require|Skip]Keywords=keyword{,keyword}..</pre>.
+     */
+    public static class Spec extends AbstractRunSpec {
+        public static final String XMLNAME = "ajc-test";
+        /**
+         * do description as title, do sourceLocation, 
+         * do keywords, do options, skip paths, do comment,
+         * skip dirChanges, do messages and do children
+         * (though we do children directly). 
+         */
+        private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
+                "title", null, null, null, "", null, true, false, false);
+        
+        private static final String OPTION_PREFIX = "-ajctest";
+        private static final String[] VALID_OPTIONS = new String[] { OPTION_PREFIX };
+
+        private static final String REQUIRE_KEYWORDS = "RequireKeywords=";
+        private static final String SKIP_KEYWORDS = "SkipKeywords=";
+        private static final String PICK_PR = "PR=";
+        private static final List VALID_SUFFIXES 
+            = Collections.unmodifiableList(Arrays.asList(new String[] 
+            { REQUIRE_KEYWORDS, SKIP_KEYWORDS, PICK_PR }));
+        
+        /** base directory of the test suite - set before making run */
+        private File suiteDir;
+        
+        /** path offset from suite directory to base of test directory */
+        String testDirOffset; // XXX revert to private after fixes
+        
+        /** id of bug - if 0, then no bug associated with this test */
+        private int bugId;
+
+        public Spec() {
+            super(XMLNAME);
+            setXMLNames(NAMES);
+        }
+        
+        public void setSuiteDir(File suiteDir) {
+            this.suiteDir = suiteDir;
+        }
+        
+        public File getSuiteDir() {
+            return suiteDir;
+        }
+        
+        /** @param bugId 100..9999 */
+        public void setBugId(int bugId) {
+            LangUtil.throwIaxIfFalse((bugId > 10) && (bugId < 10000), "bad bug id: " + bugId);
+            this.bugId = bugId;
+        }
+        
+        public int getBugId() {
+            return bugId;
+        }
+        
+        public void setTestDirOffset(String testDirOffset) {
+            if (!LangUtil.isEmpty(testDirOffset)) {
+                this.testDirOffset = testDirOffset;
+            }
+        }
+        
+        public String getTestDirOffset() {
+            return (null == testDirOffset ? "" : testDirOffset);
+        }
+        
+        /**
+         * @param sandbox ignored
+         * @see org.aspectj.testing.harness.bridge.AbstractRunSpec#makeAjcRun(Sandbox, Validator)
+         */
+        public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
+            LangUtil.throwIaxIfNull(validator, "validator");
+
+            // if no one set suiteDir, see if we have a source location
+            if (null == suiteDir) {
+                ISourceLocation loc = getSourceLocation();
+                if (!validator.nullcheck(loc, "suite file location")
+                    || !validator.nullcheck(loc.getSourceFile(), "suite file")) {
+                    return null;
+                }
+                File locDir = loc.getSourceFile().getParentFile();
+                if (!validator.canReadDir(locDir, "source location dir")) {
+                    return null;
+                }
+                suiteDir = locDir;
+            }
+
+            // we make a new sandbox with more state for our subruns, keep that, 
+            // in order to defer initialization to nextRun()
+            File testBaseDir;
+            String testDirOffset = getTestDirOffset();
+            if (LangUtil.isEmpty(testDirOffset)) {
+                testBaseDir = suiteDir;
+            } else {
+                testBaseDir = new File(suiteDir, testDirOffset);
+                if (!validator.canReadDir(testBaseDir, "testBaseDir")) {
+                    return null;
+                }
+            }
+            Sandbox childSandbox = null;
+            try {
+                childSandbox = new Sandbox(testBaseDir, validator);
+                validator.registerSandbox(childSandbox);
+            } catch (IllegalArgumentException e) {
+                validator.fail(e.getMessage());
+                return null;
+            }
+            return new AjcTest(this, childSandbox, validator);
+        }
+
+        /** @see IXmlWritable#writeXml(XMLWriter) */
+        public void writeXml(XMLWriter out) {
+            out.println("");
+            String value = (null == testDirOffset? "" : testDirOffset);
+            String attr  = out.makeAttribute("dir", value);
+            if (0 != bugId) {
+                attr += " " + out.makeAttribute("pr", ""+bugId);
+            }
+            out.startElement(xmlElementName, attr, false);
+            super.writeAttributes(out);
+            out.endAttributes();
+            super.writeChildren(out);
+            out.endElement(xmlElementName);
+        } 
+               
+        /**
+         * AjcTest overrides this to skip if 
+         * <ul>
+         * <li>the spec has a keyword the parent wants to skip</li>
+         * <li>the spec does not have keyword the parent requires</li>
+         * <li>the spec does not have the bugId required</li>
+         * </ul>
+         * @return false if this wants to be skipped, true otherwise
+         * @throws Error if selected option is not of the form
+         *          <pre>-ajctest[Require|Skip]Keywords=keyword{,keyword}..</pre>.
+         */
+        protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
+            if (!super.doAdoptParentValues(parentRuntime, handler)) {
+                return false;
+            }
+            runtime.copy(parentRuntime);
+            
+            String[] globalOptions = runtime.extractOptions(VALID_OPTIONS, true);
+            for (int i = 0; i < globalOptions.length; i++) {
+                               String option  = globalOptions[i];
+                if (!option.startsWith(OPTION_PREFIX)) {
+                    throw new Error("only expecting " + OPTION_PREFIX + "..: " + option);
+                }
+                option = option.substring(OPTION_PREFIX.length());
+                boolean keywordMustExist = false;
+                String havePr = null;
+                if (option.startsWith(REQUIRE_KEYWORDS)) {
+                    option = option.substring(REQUIRE_KEYWORDS.length());
+                    keywordMustExist = true;
+                } else if (option.startsWith(SKIP_KEYWORDS)) {
+                    option = option.substring(SKIP_KEYWORDS.length());
+                } else if (option.startsWith(PICK_PR)) {
+                    if (0 == bugId) {
+                        skipMessage(handler, "bugId required, but no bugId for this test");
+                        return false;
+                    } else {
+                        havePr = "" + bugId;
+                    }
+                    option = option.substring(PICK_PR.length());
+                } else {
+                    throw new Error("unrecognized suffix: " + globalOptions[i]
+                        + " (expecting: " + OPTION_PREFIX + VALID_SUFFIXES + "...)");
+                }
+                List specs = LangUtil.commaSplit(option);
+                // XXX also throw Error on empty specs...
+                for (Iterator iter = specs.iterator(); iter.hasNext();) {
+                                       String spec = (String) iter.next();
+                    if (null != havePr) {
+                        if (havePr.equals(spec)) { // String.equals()
+                            return true;
+                        }
+                    } else if (keywordMustExist != keywords.contains(spec)) {
+                        String reason = "keyword " + spec  
+                            + " was " + (keywordMustExist ? "not found" : "found");
+                        skipMessage(handler, reason);
+                        return false;
+                    }
+                               }
+                if (null != havePr) {
+                    skipMessage(handler, "bugId required, but not matched for this test");
+                    return false;
+                }
+            }
+            return true;
+        }  
+
+    } // AjcTest.Spec
+    
+    /** 
+     * A suite of AjcTest has children for each AjcTest
+     * and flows all options down as globals 
+     */
+    public static class Suite extends RunSpecIterator {
+        final Spec spec;
+        public Suite(Spec spec, Sandbox sandbox, Validator validator) {
+            super(spec, sandbox, validator, false);
+            this.spec = spec;
+        }
+        
+        /**
+         * While being called to make the sandbox for the child,
+         * set up the child's suite dir based on ours.
+         * @param child must be instanceof AjcTest.Spec
+                * @see org.aspectj.testing.harness.bridge.RunSpecIterator#makeSandbox(IRunSpec, Validator)
+         * @return super.makeSandbox(child, validator)
+                */
+               protected Sandbox makeSandbox(
+                       IRunSpec child,
+                       Validator validator) {
+            if (!(child instanceof AjcTest.Spec)) {
+                validator.fail("only expecting AjcTest children");
+                return null;
+            }
+            if (!validator.canReadDir(spec.suiteDir, "spec.suiteDir")) {
+                return null;
+            }
+            ((AjcTest.Spec) child).setSuiteDir(spec.suiteDir);
+                       return super.makeSandbox(child, validator);
+               }
+
+        /** 
+         * A suite spec contains AjcTest children.
+         * The suite dir or source location should be set 
+         * if the tests do not each have a source location
+         * with a source file in the suite dir.
+         * XXX whether to write out suiteDir in XML?
+         */
+        public static class Spec extends AbstractRunSpec {
+            public static final String XMLNAME = "suite";
+            /**
+             * do description, do sourceLocation, 
+             * do keywords, do options, skip paths, do comment
+             * skip dirChanges, skip messages and do children
+             * (though we do children directly). 
+             */
+            private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
+                    null, null, null, null, "", null, true, true, false);
+            File suiteDir;
+            public Spec() {
+                super(XMLNAME, false); // do not skip this even if children skip
+            }
+            
+            /** @param suiteDirPath the String path to the base suite dir */
+            public void setSuiteDir(String suiteDirPath) {
+                if (!LangUtil.isEmpty(suiteDirPath)) {
+                    this.suiteDir = new File(suiteDirPath);
+                }
+            }
+            
+            /** @param suiteDirFile the File for the base suite dir */
+            public void setSuiteDirFile(File suiteDir) {
+                this.suiteDir = suiteDir;
+            }
+            
+            /** @get suiteDir from any set or source location if set */
+            public File getSuiteDirFile() {
+                if (null == suiteDir) {
+                    ISourceLocation loc = getSourceLocation();
+                    if (null != loc) {
+                        File sourceFile = loc.getSourceFile();
+                        if (null != sourceFile) {
+                            suiteDir = sourceFile.getParentFile();
+                        }
+                    }
+                }
+                return suiteDir;
+            }
+            
+            /**
+             * @return
+             * @see org.aspectj.testing.harness.bridge.AbstractRunSpec#makeRunIterator(Sandbox, Validator)
+             */
+            public IRunIterator makeRunIterator(
+                Sandbox sandbox, 
+                Validator validator) {
+                return new Suite(this, sandbox, validator);
+            }
+
+            public String toString() {
+                // removed nKids as misleading, since children.size() may change
+                //int nKids = children.size();
+                //return "Suite.Spec(" + suiteDir + ", " + nKids + " tests)"; 
+                return "Suite.Spec(" + suiteDir + ")"; 
+            }
+        }        
+    }    
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/CompilerRun.java b/testing/src/org/aspectj/testing/harness/bridge/CompilerRun.java
new file mode 100644 (file)
index 0000000..ccb38b2
--- /dev/null
@@ -0,0 +1,966 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.ICommand;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.bridge.ReflectionFactory;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.WrappedRunIterator;
+import org.aspectj.testing.xml.SoftMessage;
+import org.aspectj.testing.xml.XMLWriter;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Run the compiler once in non-incremental mode.
+ */
+public class CompilerRun implements IAjcRun {
+       static final String[] RA_String = new String[0];
+    
+    static final String[] JAR_SUFFIXES = new String[] { ".jar", ".zip" };
+
+    static final String[] SOURCE_SUFFIXES 
+        = (String[]) FileUtil.SOURCE_SUFFIXES.toArray(new String[0]);
+        
+    /** specifications, set on construction */
+    Spec spec;
+
+    //------------ calculated during setup
+    /** get shared stuff during setup */       
+    Sandbox sandbox;
+    
+    /** 
+     * During run, these String are passed as the source and arg files to compile.
+     * The list is set up in setupAjcRun(..), when arg files are prefixed with "@".
+     */
+    final List /*String*/ arguments;
+    
+    /** 
+     * During run, these String are collapsed and passed as the injar option.
+     * The list is set up in setupAjcRun(..).
+     */
+    final List /*String*/ injars;
+   
+    /** 
+     * During run, these String are collapsed and passed as the aspectpath option.
+     * The list is set up in setupAjcRun(..).
+     */
+    final List /*String*/ aspectpath;
+
+    private CompilerRun(Spec spec) {
+        if (null == spec) {
+            throw new IllegalArgumentException("null spec");
+        }
+        this.spec = spec;
+        arguments = new ArrayList();
+        injars = new ArrayList();
+        aspectpath = new ArrayList();
+    }           
+    
+    /** 
+     * This checks that the spec is reasonable and does setup:
+     * <li>calculate and set sandbox testBaseSrcDir as 
+     *     {Sandbox.testBaseDir}/{Spec.testSrcDirOffset}/<li>
+     * <li>the list of source File to compile as
+     *     {Sandbox.testBaseSrcDir}/{Spec.getPaths..}</li>
+     * All sources must be readable at this time.
+     * @param classesDir the File
+        * @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
+     * @throws AbortException containing IOException or IllegalArgumentException
+     *          if the staging operations fail
+        */
+       public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
+
+        if (!validator.nullcheck(spec.getOptionsArray(), "localOptions")
+            || !validator.nullcheck(sandbox, "sandbox")
+            || !validator.nullcheck(spec.compiler, "compilerName")
+            || !validator.canRead(Globals.F_aspectjrt_jar, "aspectjrt.jar")
+            || !validator.canRead(Globals.F_testingclient_jar, "testing-client.jar")
+            //|| !validator.canRead(Main.F_bridge_jar, "bridge.jar")
+            ) {
+            return false;
+        }
+        
+        this.sandbox = sandbox;
+        
+        File[] cp = new File[] 
+            { sandbox.classesDir, Globals.F_aspectjrt_jar, Globals.F_testingclient_jar };
+        sandbox.setClasspath(cp, true, this);
+        
+        String rdir = spec.testSrcDirOffset;
+        File testBaseSrcDir;
+        if ((null == rdir) || (0 == rdir.length())) {
+            testBaseSrcDir = sandbox.testBaseDir;
+        } else {
+            testBaseSrcDir = new File(sandbox.testBaseDir, rdir);
+            if (!validator.canReadDir(testBaseSrcDir, "sandbox.testBaseSrcDir")) {
+                return false;
+            }
+        }
+        sandbox.setTestBaseSrcDir(testBaseSrcDir, this);
+        
+        arguments.clear();
+        
+        // sources come as relative paths - check read, copy if staging
+
+        // this renders paths absolute before run(RunStatusI) is called
+        // for a compile run to support relative paths + source base
+        // change so the run calculates the paths (differently when staging)
+
+        final String[] injarPaths; 
+        final String[] srcPaths;
+        { 
+            final String[] paths = spec.getPathsArray();
+            srcPaths = LangUtil.endsWith(paths, CompilerRun.SOURCE_SUFFIXES, true);
+            injarPaths = LangUtil.endsWith(paths, CompilerRun.JAR_SUFFIXES, true);
+        } 
+        // validate readable for sources
+        if (!validator.canRead(testBaseSrcDir, srcPaths, "sources")
+            || !validator.canRead(testBaseSrcDir, injarPaths, "injars")
+            || !validator.canRead(testBaseSrcDir, spec.argfiles, "argfiles")) {
+            return false;
+        }
+        int numSources = srcPaths.length + injarPaths.length + spec.argfiles.length;
+        if (numSources < 1) {
+            validator.fail("no source files, input jars, or arg files");
+            return false;
+        }
+        
+        File[] srcFiles;
+        File[] argFiles;
+        File[] injarFiles;
+        File[] aspectFiles;
+        if (!spec.isStaging()) { // XXX why this? was always? || (testBaseSrcDir != sandbox.stagingDir))) {
+            srcFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, srcPaths, CompilerRun.SOURCE_SUFFIXES);
+            argFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, spec.argfiles);
+            injarFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, injarPaths);
+            aspectFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, spec.aspectpath);
+        } else { // staging - copy files
+            try {
+                srcFiles = FileUtil.copyFiles(testBaseSrcDir, srcPaths, sandbox.stagingDir);
+                argFiles = FileUtil.copyFiles(testBaseSrcDir, spec.argfiles, sandbox.stagingDir);
+                injarFiles = FileUtil.copyFiles(testBaseSrcDir, injarPaths, sandbox.stagingDir);
+                aspectFiles = FileUtil.copyFiles(testBaseSrcDir, spec.aspectpath, sandbox.stagingDir);
+            } catch (IllegalArgumentException e) {
+                validator.fail("staging - bad input", e);
+                return false;
+            } catch (IOException e) {
+                validator.fail("staging - operations", e);
+                return false;
+            }
+            // validate readable for copied sources
+            if (!validator.canRead(srcFiles, "copied paths")
+                || !validator.canRead(argFiles, "copied argfiles")) {
+                return false;
+            }
+        }
+        arguments.clear();
+        injars.clear();
+        aspectpath.clear();
+        if (!LangUtil.isEmpty(srcFiles)) {
+            arguments.addAll(Arrays.asList(FileUtil.getPaths(srcFiles)));
+        }
+        if (!LangUtil.isEmpty(injarFiles)) {
+            injars.addAll(Arrays.asList(FileUtil.getPaths(injarFiles)));
+        }
+        if (!LangUtil.isEmpty(aspectFiles)) {
+            aspectpath.addAll(Arrays.asList(FileUtil.getPaths(aspectFiles)));
+        }
+        if (!LangUtil.isEmpty(argFiles)) {
+            String[] ra = FileUtil.getPaths(argFiles);
+            for (int j = 0; j < ra.length; j++) {
+                arguments.add("@" + ra[j]);
+            }
+            if (spec.isStaging) {
+                validator.info("warning: files listed in argfiles not staged");
+            }               
+        }
+
+        return true;
+    }
+//        if ((null != spec.dirChanges) && (0 < spec.numMessages(IMessage.ERROR, false))) {
+//            validator.info("CompilerRun warning: when expecting errors, cannot expect dir changes");
+//        }
+
+    
+    /**
+     * Setup result evaluation and command line, run, and evaluate result.
+     * <li>setup an AjcMessageHandler using the expected messages from
+     *     {@link Spec#getMessages()}.<li>
+     * <li>heed any globals interpreted into a TestSetup by reading
+     *     {@link Spec@getOptions()}.  For a list of supported globals, see
+     *     {@link setupArgs(ArrayList, IMessageHandler}.</li>
+     * <li>construct a command line, using as classpath 
+     *     {@link Sandbox.classpathToString()}<li>
+     * <li>construct a compiler using {@link Spec#compilerName}
+     *     or any overriding value set in TestSetup.<li>
+     * <li>Just before running, set the compiler in the sandbox using 
+     *     {@link Sandbox.setCompiler(ICommand)}.<li>
+     * <li>After running, report AjcMessageHandler results to the status parameter.
+     *     If the AjcMessageHandler reports a failure, then send info messages
+     *     for the Spec, TestSetup, and command line.<li>
+     * XXX better to upgrade AjcMessageHandler to adopt status
+     *     so the caller can control fast-fail, etc.
+        * @see org.aspectj.testing.run.IRun#run(IRunStatus)
+        */
+       public boolean run(IRunStatus status) {
+        if (null == spec.testSetup) {
+            MessageUtil.abort(status, "no test setup - adoptParentValues not called");
+            return false;
+        } else if (!spec.testSetup.result) {
+            MessageUtil.abort(status, spec.testSetup.failureReason);
+            return false;
+        }
+        AjcMessageHandler handler = new AjcMessageHandler(spec.getMessages());
+        handler.init();
+        boolean handlerResult = false;
+        boolean result = false;
+        boolean commandResult = false;
+        ArrayList argList = new ArrayList();
+        final Spec.TestSetup setupResult = spec.testSetup;
+        try {
+            argList.addAll(setupResult.commandOptions);
+            argList.add("-d");
+            String outputDirPath = sandbox.classesDir.getAbsolutePath();
+            try { // worth it to try for canonical?
+                outputDirPath = sandbox.classesDir.getCanonicalPath();        
+            } catch (IOException e) {
+                MessageUtil.abort(status, "canonical " + sandbox.classesDir, e);
+            }
+            argList.add(outputDirPath);
+            argList.add("-classpath");
+            argList.add(sandbox.classpathToString(this));
+            // XXX classpath additions here?
+
+            if (0 < injars.size()) {
+                argList.add("-injars");
+                argList.add(FileUtil.flatten((String[]) injars.toArray(new String[0]), null));
+            }
+
+            if (0 < aspectpath.size()) {
+                argList.add("-aspectpath");
+                argList.add(FileUtil.flatten((String[]) aspectpath.toArray(new String[0]), null));
+            }
+
+            // add both java/aspectj and argfiles
+            argList.addAll(arguments);
+            
+            // hack - do seeking on request as a side effect. reimplement as listener 
+            if (null != setupResult.seek) {
+                String slopPrefix = Spec.SEEK_MESSAGE_PREFIX + " slop - ";
+                PrintStream slop = MessageUtil.handlerPrintStream(
+                    status, 
+                    IMessage.INFO, 
+                    System.err, 
+                    slopPrefix);
+                List found = FileUtil.lineSeek(setupResult.seek, arguments, false, slop);
+                if (!LangUtil.isEmpty(found)) {
+                    for (Iterator iter = found.iterator(); iter.hasNext();) {
+                        MessageUtil.info(status, Spec.SEEK_MESSAGE_PREFIX + iter.next());
+                                       }
+                }
+            }
+            
+            ICommand compiler = ReflectionFactory.makeCommand(setupResult.compilerName, status);
+            DirChanges dirChanges = null;
+            if (null == compiler) {
+                MessageUtil.fail(status, "unable to make compiler " + setupResult.compilerName);
+                return false;
+            } else {
+                if (status.aborted()) {
+                    MessageUtil.debug(status, "aborted, but compiler valid?: " + compiler);
+                } else {
+                    // same DirChanges handling for JavaRun, CompilerRun, IncCompilerRun 
+                    // XXX around advice or template method/class
+                    if (!LangUtil.isEmpty(spec.dirChanges)) {
+                        LangUtil.throwIaxIfFalse(1 == spec.dirChanges.size(), "expecting 0..1 dirChanges");
+                        dirChanges = new DirChanges((DirChanges.Spec) spec.dirChanges.get(0));
+                        if (!dirChanges.start(status, sandbox.classesDir)) {
+                            return false; // setup failed
+                        }
+                    }
+                    MessageUtil.info(status, compiler + "(" + argList + ")");
+                    sandbox.setCommand(compiler, this);
+                    String[] args = (String[]) argList.toArray(RA_String);
+                    commandResult = compiler.runCommand(args, handler);
+                }
+            }
+            if (!setupResult.ignoreWarningsSet) {
+                handlerResult = handler.passed();
+            } else {
+                handlerResult = handler.passed(setupResult.ignoreWarnings);
+            }
+            if (!handlerResult) {
+                return false;
+            } else {
+                result = (commandResult == handler.expectingCommandTrue()); 
+                if (! result) {
+                    String m = commandResult 
+                        ? "compile did not fail as expected"
+                        : "compile failed unexpectedly";                        
+                    MessageUtil.fail(status, m);
+                } else if (null != dirChanges) {
+                    result = dirChanges.end(status, sandbox.testBaseDir);
+                }
+            }
+            return result;
+        } finally {
+            if (!handlerResult) { // more debugging context in case of failure
+                MessageUtil.info(handler, spec.toLongString());
+                MessageUtil.info(handler, "" + argList);
+                if (null != setupResult) {
+                    MessageUtil.info(handler, "" + setupResult);
+                }
+            }
+            handler.report(status); // XXX weak - actual messages not reported in real-time, no fast-fail
+        }             
+    }
+    
+
+    public String toString() {
+        return "CompilerRun(" + spec + ")";
+    }
+//        String[] sourcePaths = (null == this.sourcePaths ? new String[0] : this.sourcePaths);
+//        List sources = (null == this.sources ? Collections.EMPTY_LIST : this.sources);
+//        return "CompilerRun-" + compilerName 
+//            + "(" + Arrays.asList(sourcePaths)
+//            + ", " + sources
+//            + ", " + Arrays.asList(globalOptions) 
+//            + ", " + Arrays.asList(localOptions) 
+//            + ")";
+    
+    /** 
+     * Initializer/factory for CompilerRun
+     * any path or file is relative to this test base dir
+     */
+    public static class Spec extends AbstractRunSpec {
+        public static final String XMLNAME = "compile";
+        static final String SEEK_PREFIX = "-seek:";
+        static final String SEEK_MESSAGE_PREFIX = "found: ";
+        
+        /** no support in the harness for these otherwise-valid options */
+        private static final String[] INVALID_OPTIONS = new String[]
+            { "-workingdir", "-argfile", "-sourceroot", "-outjar"}; 
+            // when updating these, update tests/harness/selectionTest.xml
+
+        /** no support in the eclipse-based compiler for these otherwise-valid options */
+        private static final String[] INVALID_ECLIPSE_OPTIONS = new String[]
+            { "-lenient", "-strict", "-usejavac", "-preprocess",
+              "-XOcodeSize", "-XSerializable", "-XaddSafePrefix",
+              "-XserializableAspects", "-XtargetNearSource" };
+
+        /** options supported by the harness */
+        private static final String[] VALID_OPTIONS = new String[]
+            {
+                SEEK_PREFIX,
+                // eajc does not support -usejavac, -preprocess
+                // testFlag() handles -ajc, -eclipse, -ignoreWarnings
+                "-usejavac", "-preprocess",          
+                "-Xlint",  "-lenient", "-strict", 
+                "-source14", "-verbose", "-emacssym", 
+                "-ajc", "-eclipse", "-ignoreWarnings",
+                
+                "!usejavac", "!preprocess",          
+                "!Xlint",  "!lenient", "!strict", 
+                "!source14", "!verbose", "!emacssym", 
+                "!ajc", "!eclipse", 
+
+                "^usejavac", "^preprocess",          
+                "^Xlint",  "^lenient", "^strict", 
+                "^source14", "^verbose", "^emacssym", 
+                "^ajc", "^eclipse"
+            };
+        public static final String DEFAULT_COMPILER 
+            = ReflectionFactory.ECLIPSE;
+//            = ReflectionFactory.OLD_AJC;
+        /**
+         * Retitle description to title, paths to files, do comment
+         * do dirChanges, and print no chidren. 
+         */
+        private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
+            "title", null, null, null, "files", null, false, false, true);
+        
+        protected String compiler;
+        
+        protected TestSetup testSetup;
+        
+        protected String[] argfiles = new String[0];
+        protected String[] aspectpath = new String[0];
+        
+        /** src path = {suiteParentDir}/{testBaseDirOffset}/{testSrcDirOffset}/{path} */
+        protected String testSrcDirOffset;
+        
+        public Spec() {
+            super(XMLNAME);
+            setXMLNames(NAMES);
+            compiler = DEFAULT_COMPILER;
+        }
+        
+        public void setCompiler(String compilerName) {
+            this.compiler = compilerName;
+        }        
+
+        public void setTestSrcDirOffset(String s) {
+            if (null != s) {
+                testSrcDirOffset = s;
+            }
+        }
+        
+        /** override to set dirToken to Sandbox.CLASSES and default suffix to ".class" */
+        public void addDirChanges(DirChanges.Spec spec) {
+            if (null == spec) {
+                return;
+            }
+            spec.setDirToken(Sandbox.CLASSES_DIR);
+            spec.setDefaultSuffix(".class");
+            super.addDirChanges(spec);
+        }
+        protected String getPrintName() {
+            return "CompilerRun.Spec " + getShortCompilerName();
+        }
+        
+        public String toLongString() {
+            return getPrintName() + "(" + super.containedSummary() + ")";
+        }
+
+        public String toString() {
+            return getPrintName() + "(" + super.containedSummary() + ")";
+        }
+
+        /** bean mapping for writers */        
+        public void setFiles(String paths) {
+            addPaths(paths);
+        }
+        
+        /** 
+         * Set aspectpath, deleting any old ones
+         * @param files comma-delimited list of argfiles - ignored if null or empty
+         */        
+        public void setAspectpath(String files) {
+            if (!LangUtil.isEmpty(files)) {
+                aspectpath = XMLWriter.unflattenList(files);
+            }
+        }
+
+        /** 
+         * Set argfiles, deleting any old ones
+         * @param files comma-delimited list of argfiles - ignored if null or empty
+         */        
+        public void setArgfiles(String files) {
+            if (!LangUtil.isEmpty(files)) {
+                argfiles = XMLWriter.unflattenList(files);
+            }
+        }
+
+        /** @return String[] copy of argfiles array */
+        public String[] getArgfilesArray() {
+            String[] argfiles = this.argfiles;
+            if (LangUtil.isEmpty(argfiles)) {
+                return new String[0];
+            }
+            return (String[]) LangUtil.copy(argfiles);
+        }
+        
+        /**
+         * This implementation skips if:
+         * <ul>
+         * <li>incremental test, but using ajc (not eclipse)</li>
+         * <li>usejavac, but javac is not available on the classpath</li>
+         * <li>eclipse, but -usejavac or -preprocess test</li>
+         * <li>-source14, but running under 1.2 (XXX design)</li>
+         * <li>local/global option conflicts (-lenient/-strict)</li>
+         * <li>semantic conflicts (e.g., -lenient/-strict)</li>
+         * </ul>
+         * @return false if this wants to be skipped, true otherwise
+         */
+        protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
+            if (!super.doAdoptParentValues(parentRuntime, handler)) {
+                return false;
+            }
+            testSetup = setupArgs(handler);
+            if (!testSetup.result) {
+                skipMessage(handler, testSetup.failureReason);
+            }
+            return testSetup.result;
+        }  
+        
+
+           private String getShortCompilerName() {
+                       String cname = compiler;
+            if (null != testSetup) {
+                cname = testSetup.compilerName;
+            }
+                       if (null != cname) {
+                           int loc = cname.lastIndexOf(".");
+                           if (-1 != loc) {
+                               cname = cname.substring(loc+1);
+                           }
+                       }
+                       return cname;
+               }
+        
+        /** @return a CompilerRun with this as spec if setup completes successfully. */
+               public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
+                       CompilerRun run = new CompilerRun(this);
+            if (run.setupAjcRun(sandbox, validator)) {
+                // XXX need name for compilerRun
+                return new WrappedRunIterator(this, run);
+            }
+            return null;
+               }
+        
+        /** 
+         * Each non-incremental run, fold the global flags in with
+         * the run flags, which may involve adding or removing from
+         * either list, depending on the flag prefix:
+         * <ul>
+         * <li>-foo: use -foo unless forced off.<li>
+         * <li>^foo: (force off) remove any -foo option from the run flags</li>
+         * <li>!foo: (force on) require the -foo flag </li>
+         * </ul>
+         * If there is a force conflict, then the test is skipped
+         * ("skipping" info message, TestSetup.result is false).
+         * This means an option local to the test which was specified 
+         * without forcing may be overridden by a globally-forced option.
+         * <p>
+         * There are some flags which are interpreted by the test
+         * and removed from the list of flags passed to the command
+         * (see testFlag()):
+         * <ul>
+         * <li>eclipse: use the new eclipse compiler (can force)</li>
+         * <li>ajc: use the old ajc compiler (can force)</li>
+         * <li>ignoreWarnings: ignore warnings in result evaluations (no force)</li>
+         * </ul>
+         * <p>
+         * There are some flags which are inconsistent with each other.
+         * These are treated as conflicts and the test is skipped:
+         * <ul>
+         * <li>lenient, strict</li>
+         * </ul>
+         * <p>
+         * The -source 1.4 flag should always be specified as -source14, 
+         * as this will otherwise fail to process it correctly.  
+         * This converts it back to -source 1.4.
+         * <p>
+         * Finally, compiler limitations are enforced here by skipping
+         * tests which the compiler cannot do:
+         * <ul>
+         * <li>eclipse does not do -lenient, -strict, -usejavac, -preprocess,
+         *     -XOcodeSize, -XSerializable, XaddSafePrefix,
+         *     -XserializableAspects,-XtargetNearSource</li>
+         * <li>ajc does not run in incremental (staging) mode, 
+         *     nor with -usejavac if javac is not on the classpath</li>
+         * </ul>
+         * <u>Errors</u>:This will remove an arg not prefixed by [-|!|^] after
+         * providing an info message.
+         * <u>TestSetup Result</u>: 
+         * If this completes successfully, then TestSetup.result is true,
+         * and commandOptions is not null, and any test flags (ignore warning,
+         * compiler) are reflected in the TestSetup.
+         * If this fails, then TestSetup.result is false,
+         * and a TestSetup.failreason is set.  
+         * @return TestSetup with results 
+         *          (TestSetup result=false if the run should not continue)
+         */
+        protected TestSetup setupArgs(IMessageHandler handler) {
+            // warning: HarnessSelectionTest checks for specific error wording
+            ArrayList argList = new ArrayList();
+            argList.addAll(getOptionsList());
+            final Spec spec = this;
+            TestSetup result = new TestSetup();
+            if (argList.contains("-source")) {
+                result.failureReason = "use -source14 for -source 1.4: " + argList;
+                return result;                
+            }
+            result.compilerName = spec.compiler;
+            String[] globalOptions = spec.runtime.extractOptions(Spec.VALID_OPTIONS, true);
+            if ((null != globalOptions) && (globalOptions.length > 0)) {
+                // --- fold in globals, removing conflicts, etc.
+                for (int i = 0; i < globalOptions.length; i++) {
+                    String globalArg = globalOptions[i];
+                    if ((null == globalArg) || (2 > globalArg.length())) {
+                        continue;
+                    } else if (globalArg.startsWith(SEEK_PREFIX)) { 
+                        result.seek = globalArg.substring(SEEK_PREFIX.length());
+                        continue;
+                    } else if ("-source".equals(globalArg)) {
+                        result.failureReason = "use -source14 for -source 1.4 [" + i + "]";
+                        return result;                
+                    }
+                    char first = globalArg.charAt(0);
+                    globalArg = globalArg.substring(1);
+                    boolean globalForceOn   = (first == '!');
+                    boolean globalForceOff  = (first == '^');
+                    boolean globalSet       = (first == '-');
+                    if (!globalSet && !globalForceOn && !globalForceOff) {
+                        MessageUtil.info(handler, "ignoring bad global: " + globalOptions[i]);
+                        continue;
+                    }
+                    int argIndex = indexOf(globalArg, argList);
+                    if (-1 == argIndex) { // no apparent conflict - look for eclipse/ajc conflicts XXX unresolved
+                        boolean ajcGlobal = true;
+                        if ("ajc".equals(globalArg)) {
+                            argIndex = indexOf("eclipse", argList);
+                        } else if ("eclipse".equals(globalArg)) {
+                            argIndex = indexOf("ajc", argList);                            
+                            ajcGlobal = false;
+                        }
+                        if (-1 != argIndex) {   // resolve eclipse/ajc conflict
+                            String arg = ((String) argList.get(argIndex));
+                            char argFirst = arg.charAt(0);
+                            argList.remove(arg);      // replace with resolved variant...
+                            char ajcFirst;
+                            char eclipseFirst;
+                            if (ajcGlobal) {
+                                ajcFirst = first;
+                                eclipseFirst = argFirst;
+                            } else {
+                                ajcFirst = argFirst;
+                                eclipseFirst = first;
+                            }
+                            if ('!' == eclipseFirst) {
+                                if ('!' == ajcFirst) {
+                                    result.failureReason = "conflict between !eclipse and !ajc";
+                                    return result;
+                                } else {
+                                    argList.add("-eclipse");
+                                }
+                            } else if (('!' == ajcFirst)  || ('^' == eclipseFirst)) {
+                                argList.add("-ajc");
+                            } else if ('^' == ajcFirst) {
+                                argList.add("-eclipse");
+                            } else if (('-' != ajcFirst) || ('-' != eclipseFirst)) {
+                                result.failureReason = "harness logic error resolving "
+                                    + arg + " and global " + globalArg;
+                                return result;
+                            } else if (ajcGlobal) {
+                                argList.add("-ajc");
+                            } else {
+                                argList.add("-eclipse");
+                            }
+                            continue; // resolved 
+                        }
+                    }
+                    
+                    if (-1 == argIndex) { // no dup, so no conflict
+                        if (!globalForceOff) {
+                            argList.add("-" + globalArg);
+                        }
+                    } else { // have conflict - resolve
+                        String arg = (String) argList.get(argIndex);
+                        first = arg.charAt(0);
+                        boolean localForceOn   = (first == '!');
+                        boolean localForceOff  = (first == '^');
+                        boolean localSet       = (first == '-');
+                        if (!localSet && !localForceOn && !localForceOff) {
+                            result.failureReason = "only handling [-^!]{arg}: " + arg;
+                            return result;
+                        }
+                        if ((localForceOn && globalForceOff)
+                            || (localForceOff && globalForceOn)) {
+                            result.failureReason = "force conflict between arg=" 
+                                + arg + " and global=" + globalOptions[i];
+                            return result;
+                        } 
+                        if (globalForceOn) {
+                            if (localForceOn) { // localSet is already correct, localForceOff was conflict
+                                argList.remove(arg);      // no !funkiness
+                                argList.add("-" + globalArg);
+                            }
+                        } else if (globalSet) {
+                            if (localSet) {
+                                // do nothing - already correct
+                            } else if (localForceOn) {
+                                argList.remove(arg);      // no !funkiness
+                                argList.add("-" + globalArg);
+                            }
+                        } else if (globalForceOff) {
+                            argList.remove(arg);
+                        } else {
+                            throw new Error("illegal arg state?? : " + arg);
+                            //MessageUtil.info(handler, "illegal arg state??: " + arg);
+                        }
+                    }
+                }
+            }
+            
+            // remove funky prefixes from remainder, fixup two-part flags
+            // and interpret special flags
+            boolean source14 = false;
+            ArrayList toAdd = new ArrayList();
+            for (ListIterator iter = argList.listIterator(); iter.hasNext();) {
+                String arg = (String) iter.next();
+                if (testFlag(arg, result)) {
+                    iter.remove();
+                    continue;
+                }
+                char c = arg.charAt(0);
+                String rest = arg.substring(1);
+                if (c == '^') {
+                    iter.remove();
+                    continue;
+                }
+                if (c == '!') {
+                    iter.remove();
+                    if (!("source14".equals(rest))) {
+                        toAdd.add("-" + rest);
+                    } else {
+                        source14 = true;
+                    }
+                    rest = null;
+                } else if ("source14".equals(rest)) {
+                    iter.remove();
+                    source14 = true;
+                }
+            }
+            if (source14) {
+                // must run under 1.4 VM or (if ajc) set up bootclasspath
+                if (!LangUtil.supportsJava("1.4")) {
+                    if (ReflectionFactory.ECLIPSE.equals(result.compilerName)) {
+                        result.failureReason 
+                            = "eclipse must run under 1.4 to implement -source 1.4";
+                        return result;                        
+                    }
+                    
+                    if (!FileUtil.canReadFile(Globals.J2SE14_RTJAR)) {
+                        result.failureReason 
+                            = "unable to get 1.4 libraries to implement -source 1.4";
+                        return result;
+                    }
+                    toAdd.add("-bootclasspath");
+                    toAdd.add(Globals.J2SE14_RTJAR.getAbsolutePath());
+                }
+                toAdd.add("-source");
+                toAdd.add("1.4");
+            }
+            argList.addAll(toAdd);
+    
+            // finally, check for semantic conflicts
+            String[] badOptions = LangUtil.selectOptions(argList, Spec.INVALID_OPTIONS);
+            if (!LangUtil.isEmpty(badOptions)) {
+                result.failureReason = "no support for (normally-valid) options "
+                     + Arrays.asList(badOptions);
+            } else if (argList.contains("-lenient") && argList.contains("-strict")) {
+                result.failureReason = "semantic conflict -lenient | -strict";
+            } else if (ReflectionFactory.OLD_AJC.equals(result.compilerName)) {
+                if (spec.isStaging) {
+                    result.failureReason = "OLD_AJC does not do incremental compiles";
+                } else if (argList.contains("-usejavac") && !haveJavac()) {
+                    result.failureReason = "-usejavac but no javac on classpath";
+                } else {
+                    result.result = true;
+                }
+            } else if (!ReflectionFactory.ECLIPSE.equals(result.compilerName)) {
+                result.failureReason = "unrecognized compiler: " + result.compilerName;
+            } else {
+                badOptions = LangUtil.selectOptions(argList, Spec.INVALID_ECLIPSE_OPTIONS);
+                if (!LangUtil.isEmpty(badOptions)) {                    
+                    result.failureReason = "no support in eclipse-based compiler"
+                        + " for (normally-valid) options "+ Arrays.asList(badOptions);
+                } else {
+                    result.result = true;
+                }
+            }
+            if (result.result) {
+                result.commandOptions = argList;
+            }
+            return result; 
+        }
+
+               /** @return true if javac is available on the classpath */
+               private boolean haveJavac() { // XXX copy/paste from JavaCWrapper.java
+            Class compilerClass = null;
+            try {
+                compilerClass = Class.forName("com.sun.tools.javac.Main");
+            } catch (ClassNotFoundException ce1) {
+                try {
+                    compilerClass = Class.forName("sun.tools.javac.Main");
+                } catch (ClassNotFoundException ce2) {
+                }
+            }
+            return (null != compilerClass);
+               }
+
+        
+        /**
+         * Handle flags that are interpreted by the test rather than the
+         * underlying command.  These flags are to be removed from the
+         * arg list.
+         *  @return true if this is a flag to remove from the arg list
+         */
+        protected boolean testFlag(String arg, TestSetup result) {
+            if ("-eclipse".equals(arg) || "!eclipse".equals(arg) || "^ajc".equals(arg)) {
+                result.compilerName = ReflectionFactory.ECLIPSE;
+                return true;
+            } else if ("-ajc".equals(arg) || "!ajc".equals(arg) || "^eclipse".equals(arg)) {
+                result.compilerName = ReflectionFactory.OLD_AJC;
+                return true;
+            } else if ("-ignoreWarnings".equals(arg)) {
+                result.ignoreWarnings = true;
+                result.ignoreWarningsSet = true;
+                return true;
+            }
+            return false;
+        }
+        
+        // XXX need keys, cache...
+        /** @return index of global in argList, ignoring first char */
+        protected int indexOf(String global, ArrayList argList) {
+            int max = argList.size();
+            for (int i = 0; i < max; i++) {
+                 if (global.equals(((String) argList.get(i)).substring(1))) {
+                    return i;
+                 }
+            }
+            return -1;
+        }
+        
+        /** 
+         * Write this out as a compile element as defined in
+         * AjcSpecXmlReader.DOCTYPE.
+         * @see AjcSpecXmlReader#DOCTYPE 
+         * @see IXmlWritable#writeXml(XMLWriter) 
+         */
+        public void writeXml(XMLWriter out) {
+            // If our state is empty, we just delegate to super.
+            if (DEFAULT_COMPILER.equals(compiler) 
+                && LangUtil.isEmpty(testSrcDirOffset)
+                && LangUtil.isEmpty(argfiles)) {
+                super.writeXml(out);
+                return;
+            } 
+            String attr = "";
+            out.startElement(xmlElementName, false);
+            if (!DEFAULT_COMPILER.equals(compiler)) {
+                out.printAttribute("compiler", compiler);
+            }
+            if (!LangUtil.isEmpty(testSrcDirOffset)) {
+                out.printAttribute("dir", testSrcDirOffset);
+            }
+            if (!LangUtil.isEmpty(argfiles)) {
+                out.printAttribute("argfiles", out.flattenFiles(argfiles));
+            }
+            super.writeAttributes(out);
+            out.endAttributes();
+            if (!LangUtil.isEmpty(dirChanges)) {
+                DirChanges.Spec.writeXml(out, dirChanges);
+            }
+            List messages = getMessages();
+            if (!LangUtil.isEmpty(messages)) {
+                SoftMessage.writeXml(out, messages);
+            }
+            out.endElement(xmlElementName);
+        }
+        
+        /** 
+         * Encapsulate the directives that can be set using
+         * global arguments supplied in {@link Spec.getOptions()}.
+         * This supports changing the compiler and ignoring warnings.
+         */
+        class TestSetup {
+            /** null unless overriding the compiler to be used */
+            String compilerName;
+            /** 
+             * true if we should tell AjcMessageHandler whether
+             * to ignore warnings in its result evaluation
+             */
+            boolean ignoreWarningsSet;
+            
+            /** if telling AjcMessageHandler, what we tell it */
+            boolean ignoreWarnings;
+            
+            /** false if setup failed */
+            boolean result;
+            
+            /** if setup failed, this has the reason why */
+            String failureReason;
+            
+            /** beyond running test, also seek text in sources */
+            String seek;
+            
+            /** if setup completed, this has the combined global/local options */
+            ArrayList commandOptions;
+            
+            public String toString() {
+                return "TestSetup("
+                    + (null == compilerName ? "" : compilerName + " ")
+                    + (!ignoreWarningsSet ?  "" 
+                        : (ignoreWarnings ? "" : "do not ")
+                            + "ignore warnings ")
+                    + (result ? "" : "setup failed")
+                    + ")";
+            }
+        }
+    } // CompilerRun.Spec
+} // CompilerRun
+        
+//        /** 
+//         * Write this out as a compile element as defined in
+//         * AjcSpecXmlReader.DOCTYPE.
+//         * @see AjcSpecXmlReader#DOCTYPE 
+//         * @see IXmlWritable#writeXml(XMLWriter) 
+//         */
+//        public void writeXml(XMLWriter out) {
+//            StringBuffer sb = new StringBuffer();
+//            Spec spec = this;
+//            final String elementName = "compile";
+//            List list = spec.getOptionsList();
+//            String args = XMLWriter.flattenList(spec.getOptionsList()).trim();
+//            String argsAttr = out.makeAttribute("options", args).trim();
+//            String files = XMLWriter.flattenFiles(spec.getPathsArray()).trim();
+//            String filesAttr = out.makeAttribute("files", files).trim();
+//            List messages = spec.getMessages();
+//            int nMessages = messages.size();
+//            int both = argsAttr.length() + filesAttr.length();
+//            final int MAX = 55;
+//    
+//            // tortured logic to make more readable XML...        
+//            if ((both < MAX) || (0 == args.length())) {
+//                // if short enough, print entire or just start
+//                if (0 != args.length()) {
+//                    filesAttr = argsAttr + " " + filesAttr;
+//                }
+//                if (0 == nMessages) {
+//                    out.printElement(elementName, filesAttr);
+//                    return;
+//                } else {
+//                    out.startElement(elementName, filesAttr, true);
+//                }
+//            } else if (argsAttr.length() < filesAttr.length()) {
+//                out.startElement(elementName, argsAttr, false);
+//                out.printAttribute("files", files);   
+//                out.endAttributes();
+//            } else { 
+//                out.startElement(elementName, filesAttr, false);
+//                out.printAttribute("options", args);   
+//                out.endAttributes();
+//            }
+//            if (0 < nMessages) {
+//                SoftMessage.writeXml(out, messages);
+//            }
+//            out.endElement(elementName);
+//        }
+
diff --git a/testing/src/org/aspectj/testing/harness/bridge/DirChanges.java b/testing/src/org/aspectj/testing/harness/bridge/DirChanges.java
new file mode 100644 (file)
index 0000000..73b930d
--- /dev/null
@@ -0,0 +1,522 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.util.TestUtil;
+import org.aspectj.testing.xml.IXmlWritable;
+import org.aspectj.testing.xml.XMLWriter;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Calculate changes in a directory tree.
+ * Usage:
+ * <ul>
+ * <li>Set up with any expected changes and/or an expected directory</li>
+ * <li>Set up with any file checker</li>
+ * <li>start(..) before changes. 
+ *     This issues messages for any removed files not found,
+ *     which represent an error in the expected changes.</li>
+ * <li>Do whatever operations will change the directory</li>
+ * <li>end(..).
+ *     This issues messages for any files not removed, added, or updated,
+ *     and, if any checker was set, any checker messages for matching
+ *     added or updated files</li>
+ * </ul>
+ */
+public class DirChanges {
+    
+    private static final boolean EXISTS = true;
+
+    final Spec spec;
+    
+    /** start time, in milliseconds - valid only from start(..)..end(..) */
+    long startTime;
+    
+    /** base directory of actual files - valid only from start(..)..end(..) */
+    File baseDir;
+    
+    /** if set, this is run against any resulting existing files */
+    IFileChecker fileChecker;
+    
+    /** handler valid from start..end of start(..) and end(..) methods */
+    IMessageHandler handler;
+    
+       /**
+        * Constructor for DirChanges.
+        */
+       public DirChanges(Spec spec) {
+               LangUtil.throwIaxIfNull(spec, "spec");
+        this.spec = spec;
+    }
+
+    /**
+     * Inspect the base dir, and issue any messages for
+     * removed files not present.
+     * @param baseDir File for a readable directory
+     * @return true if this started without sending messages
+     *          for removed files not present.
+     */
+    public boolean start(IMessageHandler handler, File baseDir) {
+        FileUtil.throwIaxUnlessCanReadDir(baseDir, "baseDir");
+        final IMessageHandler oldHandler = this.handler;
+        this.handler = handler;
+        this.baseDir = baseDir;
+        startTime = 0l;
+        final boolean doCompare = false;
+        boolean result 
+                = exists("at start, did not expect file to be added", !EXISTS, spec.added, doCompare);            
+        result &= exists("at start, expected file to be unchanged", EXISTS, spec.unchanged, doCompare);            
+        result &= exists("at start, expected file to be updated", EXISTS, spec.updated, doCompare);            
+        result &= exists("at start, expected file to be removed", EXISTS, spec.removed, doCompare);            
+        startTime = System.currentTimeMillis();
+        this.handler = oldHandler;
+        return result;        
+    }
+
+    
+    /**
+     * Inspect the base dir, issue any messages for
+     * files not added, files not updated, and files not removed,
+     * and compare expected/actual files added or updated.
+     * @throws IllegalStateException if called before start(..)
+     */
+    public boolean end(IMessageHandler handler, File srcBaseDir) {
+        FileUtil.throwIaxUnlessCanReadDir(baseDir, "baseDir");
+        if (0l == startTime) {
+            throw new IllegalStateException("called before start");
+        }
+        final IMessageHandler oldHandler = this.handler;
+        this.handler = handler;
+        final boolean doCompare = (null != fileChecker);
+        final boolean fastFail = spec.fastFail;
+        boolean result
+                = exists("at end, expected file was not added",   EXISTS, spec.added, doCompare);            
+        if (result || !fastFail) {
+            result &= exists("at end, expected file was not unchanged", EXISTS, spec.unchanged, doCompare, false);            
+        }
+        if (result || !fastFail) {
+            result &= exists("at end, expected file was not updated", EXISTS, spec.updated, doCompare);            
+        }
+        if (result || !fastFail) {
+            result &= exists("at end, file exists, was not removed", !EXISTS, spec.removed, doCompare); 
+        }
+        if (result || !fastFail) {
+            result &= compareDir(srcBaseDir);          
+        }
+        // XXX validate that unchanged mod-time did not change
+
+        this.handler = oldHandler;
+        baseDir = null;
+        startTime = 0l;
+        return result;
+    }
+
+       /**
+     * Verify that all files in any specified expected directory
+     * have matching files in the base directory, putting any messages
+     * in the handler (only one if the spec indicates fast-fail).
+        * @param srcBaseDir the File for the base directory of the test sources
+     *         (any expected dir is specified relative to this directory)
+        * @return true if the same, false otherwise
+        */
+       private boolean compareDir(File srcBaseDir) {
+        if (null == spec.dirExpected) {
+            return true;
+        }
+        File expDir = new File(srcBaseDir, spec.dirExpected);
+        File actDir = baseDir;
+        return TestUtil.sameDirectoryContents(handler, expDir, actDir, spec.fastFail);
+       }
+
+
+    /** @param comp FileMessageComparer (if any) given matching messages to compare */
+    protected void setFileComparer(IFileChecker comp) {
+        this.fileChecker = comp;
+    }
+    
+
+
+    /**
+     * Signal fail if any files do {not} exist or do {not} have last-mod-time after startTime
+     * @param handler the IMessageHandler sink for messages
+     * @param label the String infix for the fail message
+     * @param exists if true, then file must exist and be modified after start time;
+     *                if false, then the file must not exist or must be modified before start time.
+     * @param pathList the List of path (without any Spec.defaultSuffix) of File
+     *                  in Spec.baseDir to find (or not, if !exists)
+     */
+    protected boolean exists(
+        String label,
+        boolean exists,
+        List pathList,
+        boolean doCompare) {
+        boolean expectStartEarlier = true;
+       return exists(label, exists, pathList,doCompare, true);    
+    }
+    protected boolean exists(
+        String label,
+        boolean exists,
+        List pathList,
+        boolean doCompare,
+        boolean expectStartEarlier) {
+        boolean result = true;
+        if (!LangUtil.isEmpty(pathList)) {
+            final File expDir = ((!doCompare || (null == spec.dirExpected))
+                ? null 
+                : new File(baseDir, spec.dirExpected));
+            for (Iterator iter = pathList.iterator(); iter.hasNext();) {
+                final String entry = (String) iter.next() ;
+                String path = entry ;
+                if (null != spec.defaultSuffix) {
+                    if (".class".equals(spec.defaultSuffix)) {
+                        path = path.replace('.', '/');
+                    }
+                    path = path + spec.defaultSuffix;
+                }
+                File actualFile = new File(baseDir, path);
+                if (exists != (actualFile.canRead() && actualFile.isFile() 
+                                && (expectStartEarlier
+                                       ? startTime <= actualFile.lastModified()
+                                       : startTime > actualFile.lastModified()
+                                       ))) {
+                    failMessage(handler, exists, label, path, actualFile); 
+                    if (result) {
+                        result = false;
+                    }
+                } else if (exists && doCompare && (null != fileChecker)) {
+                    if (!fileChecker.checkFile(handler, path, actualFile) && result) {
+                        result = false;
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Generate fail message "{un}expected {label} file {path} in {baseDir}".
+     * @param handler the IMessageHandler sink
+     * @param label String message infix 
+     * @param path the path to the file
+     */
+    protected void failMessage(
+        IMessageHandler handler,
+        boolean exists,
+        String label,
+        String path, 
+        File file) {
+        MessageUtil.fail(handler, label + " \"" + path + "\" in " + baseDir); 
+    }
+
+
+    /** Check actual File found at a path, usually to diff expected/actual contents */
+    public static interface IFileChecker {
+        /** 
+         * Check file found at path.
+         * Implementations should return false when adding fail (or worse)
+         * message to the handler, and true otherwise.
+         * @param handler IMessageHandler sink for messages, esp. fail.
+         * @param path String for normalized path portion of actualFile.getPath()
+         * @param actualFile File to file found
+         * @return false if check failed and messages added to handler
+         */
+        boolean checkFile(IMessageHandler handler, String path, File actualFile);
+    }  
+    
+//    /**
+//     * Default FileChecker compares files literally, transforming any 
+//     * with registered normalizers.
+//     */
+//    public static class FileChecker implements IFileChecker {
+//        final File baseExpectedDir;
+//        NormalizedCompareFiles fileComparer;
+//
+//        public FileChecker(File baseExpectedDir) {
+//            this.baseExpectedDir = baseExpectedDir;    
+//            fileComparer = new NormalizedCompareFiles();
+//        }
+//        public boolean checkFile(IMessageHandler handler, String path, File actualFile) {
+//            if (null == baseExpectedDir) {
+//                MessageUtil.error(handler, "null baseExpectedDir set on construction");
+//            } else if (!baseExpectedDir.canRead() || !baseExpectedDir.isDirectory()) {
+//                MessageUtil.error(handler, "bad baseExpectedDir: " + baseExpectedDir);
+//            } else {
+//                File expectedFile = new File(baseExpectedDir, path);
+//                if (!expectedFile.canRead()) {
+//                    MessageUtil.fail(handler, "cannot read expected file: " + expectedFile);
+//                } else {
+//                    return doCheckFile(handler, expectedFile, actualFile, path);
+//                }
+//            }
+//            return false;
+//        }
+//
+//        protected boolean doCheckFile(
+//            IMessageHandler handler, 
+//            File expectedFile, 
+//            File actualFile,
+//            String path) {
+//            fileComparer.setHandler(handler);
+//            FileLine[] expected = fileComparer.diff();
+//            return false;
+//        }
+//    }
+    
+//    /**
+//     * CompareFiles implementation that pre-processes input 
+//     * to normalize it.  Currently it reads all files except
+//     * .class files, which it disassembles first.
+//     */
+//    public static class NormalizedCompareFiles extends CompareFiles {
+//        private final static String[] NO_PATHS = new String[0];
+//        private static String normalPath(File file) { // XXX util
+//            if (null == file) {
+//                return "";
+//            }
+//            return file.getAbsolutePath().replace('\\', '/');
+//        }
+//        
+//        private String[] baseDirs;
+//        private IMessageHandler handler;
+//        
+//        public NormalizedCompareFiles() {
+//        }
+//        
+//        void init(IMessageHandler handler, File[] baseDirs) {
+//            this.handler = handler;
+//            if (null == baseDirs) {
+//                this.baseDirs = NO_PATHS;
+//            } else {
+//                this.baseDirs = new String[baseDirs.length];
+//                for (int i = 0; i < baseDirs.length; i++) {
+//                                     this.baseDirs[i] = normalPath(baseDirs[i]) + "/";
+//                             }
+//            }
+//        } 
+//        
+//        private String getClassName(File file) {
+//            String result = null;
+//            String path = normalPath(file);
+//            if (!path.endsWith(".class")) {
+//                    MessageUtil.error(handler, 
+//                        "NormalizedCompareFiles expected " 
+//                        + file
+//                        + " to end with .class"); 
+//            } else {
+//                path = path.substring(0, path.length()-6);
+//                for (int i = 0; i < baseDirs.length; i++) {
+//                    if (path.startsWith(baseDirs[i])) {
+//                        return path.substring(baseDirs[i].length()).replace('/', '.');
+//                    }
+//                }
+//                MessageUtil.error(handler, 
+//                    "NormalizedCompareFiles expected " 
+//                    + file
+//                    + " to start with one of " 
+//                    + LangUtil.arrayAsList(baseDirs));
+//            }
+//        
+//            return result;
+//        }
+//        
+//        /** 
+//         * Read file as normalized lines, sending handler any messages
+//         * ERROR for input failures and FAIL for processing failures.
+//         * @return NOLINES on error or normalized lines from file otherwise 
+//         */
+//        public FileLine[] getFileLines(File file) {
+//            FileLineator capture = new FileLineator();
+//            InputStream in = null;
+//            try { 
+//                if (!file.getPath().endsWith(".class")) {
+//                    in = new FileInputStream(file);
+//                    FileUtil.copyStream(
+//                        new BufferedReader(new InputStreamReader(in)), 
+//                        new PrintWriter(capture));
+//                } else {
+//                    String name = getClassName(file);
+//                    if (null == name) {
+//                        return new FileLine[0];
+//                    }
+//                    String path = normalPath(file);
+//                    path = path.substring(0, path.length()-name.length());
+//                    // XXX  sole dependency on bcweaver/bcel
+//                    LazyClassGen.disassemble(path, name, capture);
+//                }
+//            } catch (IOException e) {
+//                MessageUtil.fail(handler, 
+//                    "NormalizedCompareFiles IOException reading " + file, e);
+//                return null;
+//            } finally {
+//                if (null != in) {
+//                    try { in.close(); }
+//                    catch (IOException e) {} // ignore
+//                }
+//                capture.flush();
+//                capture.close();
+//            }
+//            String missed = capture.getMissed();
+//            if (!LangUtil.isEmpty(missed)) {
+//                MessageUtil.fail(handler, 
+//                    "NormalizedCompareFiles missed input: " 
+//                    + missed);
+//                return null;
+//            } else {
+//                return capture.getFileLines();
+//            }                    
+//        } 
+//    
+//        
+//    }           
+    /**
+     * Specification for a set of File added, removed, or updated
+     * in a given directory, or for a directory base for a tree of expected files. 
+     * If defaultSuffix is specified, entries may be added without it.
+     */
+    public static class Spec implements IXmlWritable {
+        public static final String XMLNAME = "dir-changes";
+        String dirToken;
+        String defaultSuffix;
+        String dirExpected;
+        /** if true, ok to fail on first mis-match */
+        boolean fastFail;
+        final ArrayList added;
+        final ArrayList removed;
+        final ArrayList updated;
+        final ArrayList unchanged;
+    
+        /**
+         * @param dirToken the symbol name of the base directory
+         * @param clipSuffix the String suffix, if any, to clip automatically
+         */
+        public Spec() {
+            added = new ArrayList();
+            removed = new ArrayList();
+            updated = new ArrayList();
+            unchanged = new ArrayList();
+        }
+        
+        /**
+         * @param dirToken the symbol name of the base directory (classes, run)
+         */
+        public void setDirToken(String dirToken) {
+            this.dirToken = dirToken;
+        }
+        
+        /**
+         * Set the directory containing the expected files.
+         * Currently this only is used to verify files that are expected 
+         * and found after the process completes; it does not stand on its
+         * own as a specification of files.
+         * @param expectedDirRelativePath path relative to the test base
+         *         of the directory containing expected results for the output dir.
+         */
+        public void setExpDir(String expectedDirRelativePath) {
+        }
+        
+        /**
+         * @param clipSuffix the String suffix, if any, to clip automatically
+         */
+        public void setDefaultSuffix(String defaultSuffix) {
+            this.defaultSuffix = defaultSuffix;
+        }
+        
+        public void setAdded(String items) {
+            XMLWriter.addFlattenedItems(added, items);
+        }
+        
+        public void setRemoved(String items) {
+            XMLWriter.addFlattenedItems(removed, items);
+        }
+        
+        public void setUpdated(String items) {
+            XMLWriter.addFlattenedItems(updated, items);
+        }
+        
+        public void setUnchanged(String items) {
+            XMLWriter.addFlattenedItems(unchanged, items);
+        }
+        public void setFastfail(boolean fastFail) {
+            this.fastFail = fastFail; // XXX support in XML
+        }
+        
+        /** @return true if some list was specified */
+        private boolean hasFileList() {
+            return (!LangUtil.isEmpty(added) 
+                       || !LangUtil.isEmpty(removed)
+                    || !LangUtil.isEmpty(updated)
+                    || !LangUtil.isEmpty(unchanged)
+                    );
+        }
+        
+        /** this writes nothing if there are no added, removed, or changed */
+        public void writeXml(XMLWriter out) {
+            if (!hasFileList()) {
+                return;
+            }
+            String attr = "";
+            // XXX need to permit defaults here...
+            if (null != dirToken) {
+                attr = out.makeAttribute("dirToken", dirToken) + " ";
+            }
+            if (null != defaultSuffix) {
+                attr += out.makeAttribute("defaultSuffix", defaultSuffix);
+            }
+            out.startElement(XMLNAME, attr, false);
+            if (!LangUtil.isEmpty(added)) {
+                out.printAttribute("added", XMLWriter.flattenList(added));
+            }
+            if (!LangUtil.isEmpty(removed)) {
+                out.printAttribute("removed", XMLWriter.flattenList(removed));
+            }
+            if (!LangUtil.isEmpty(updated)) {
+                out.printAttribute("updated", XMLWriter.flattenList(updated));
+            }
+            if (!LangUtil.isEmpty(unchanged)) {
+                out.printAttribute("unchanged", XMLWriter.flattenList(unchanged));
+            }
+            out.endElement(XMLNAME);
+        }
+        
+        /**
+                * Write list as elements to XMLWriter.
+                * @param out XMLWriter output sink
+                * @param dirChanges List of DirChanges.Spec to write
+                */
+               public static void writeXml(XMLWriter out, List dirChanges) {
+            if (LangUtil.isEmpty(dirChanges)) {
+                return;
+            }
+            LangUtil.throwIaxIfNull(out, "out");
+            for (Iterator iter = dirChanges.iterator(); iter.hasNext();) {
+                               DirChanges.Spec spec = (DirChanges.Spec) iter.next();
+                               if (null == spec) {
+                    continue;
+                }
+                spec.writeXml(out);
+                       }            
+               }
+
+} // class Spec
+
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/FlatSuiteReader.java b/testing/src/org/aspectj/testing/harness/bridge/FlatSuiteReader.java
new file mode 100644 (file)
index 0000000..dbf5dd2
--- /dev/null
@@ -0,0 +1,377 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.Message;
+import org.aspectj.bridge.SourceLocation;
+import org.aspectj.bridge.IMessage.Kind;
+import org.aspectj.testing.util.BridgeUtil;
+import org.aspectj.testing.util.ObjectChecker;
+import org.aspectj.testing.util.SFileReader;
+import org.aspectj.testing.util.StandardObjectChecker;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+import org.aspectj.util.LineReader;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/** 
+ * SFileReader.Maker implementation to read tests 
+ * XXX supports iterative but not yet incremental compiles
+ */
+public class FlatSuiteReader implements SFileReader.Maker {
+       public static final String[] RA_String = new String[0];
+       public static final FlatSuiteReader ME = new FlatSuiteReader();
+       private static final SFileReader READER = new SFileReader(ME);
+
+       static boolean isNumber(String s) { // XXX costly
+               if ((null == s) || (0 == s.length())) {
+                       return false;
+               }
+               try {
+                       Integer.valueOf(s);
+                       return true;
+               } catch (NumberFormatException e) {
+                       return false;
+               }
+       }
+
+    /** if true, clean up records before returning from make */
+    public boolean clean;
+    
+       private FlatSuiteReader() {
+       }
+
+       /**
+        * @see org.aspectj.testing.harness.bridge.SFileReader.Maker#getType()
+        */
+       public Class getType() {
+               return AjcTest.Spec.class;
+       }
+
+       /**
+        * This constructs an AjcTest.Spec assuming we are at the start of a
+        * test definition in reader and taking the parent directory of
+        * the reader as the base directory for the test suite root. 
+        * @return the next AjcTest in reader, or null
+        * @see org.aspectj.testing.harness.bridge.SFileReader.Maker#make(LineReader)
+        */
+       public Object make(final LineReader reader)
+               throws AbortException, IOException {
+               final AjcTest.Spec result = new AjcTest.Spec();
+               boolean usingEclipse = false; // XXX
+               /** handle read errors by throwing AbortException with context info */
+               class R {
+                       public String read(String context) throws IOException {
+                               return read(context, true);
+                       }
+                       public String read(String context, boolean required)
+                               throws IOException {
+                               final boolean skipEmpties = false;
+                               String result = reader.nextLine(skipEmpties);
+                               if ((null != result) && (0 == result.length())) {
+                                       result = null;
+                               }
+                               if ((null == result) && required) {
+                                       String s = "expecting " + context + " at " + reader;
+                                       throw new AbortException(s);
+                               }
+                               return result;
+                       }
+               }
+
+               final R r = new R();
+               //final String baseDir = reader.getFile().getParent();
+               String line;
+               String[] words;
+               boolean isRequired = true;
+
+               final int startLine = reader.getLineNumber() - 1;
+
+               // description first - get from last line read
+               // XXX permits exactly one blank line between test records?
+               result.description = reader.lastLine();
+               if (null == result.description) {
+                       throw new AbortException("expecting description at " + reader);
+               }
+
+               // next line is baseDir {option..}
+               line = r.read("baseDir {option..}");
+               words = LangUtil.split(line);
+               if ((null == words) || (0 == words.length)) {
+                       throw new AbortException(
+                               "expecting dir {option..} at " + reader);
+               }
+               // XXX per-test (shared) root
+               //final File sourceRoot =  new File(baseDir, words[0]);
+               result.setTestDirOffset(words[0]);
+
+               String[] compileOptions = new String[words.length - 1];
+               System.arraycopy(words, 1, compileOptions, 0, words.length - 1);
+
+               // next are 1..n source lines: source...
+               CompilerRun.Spec lastCompileSpec = null;
+               // save last source file as default for error/warning line
+               File lastFile = null; // XXX per-compiler-run errors
+               while (null != (line = r.read("source.."))) {
+                       words = LangUtil.split(line);
+                       if (0 == FileUtil.sourceSuffixLength(words[0])) { // XXX
+                               break;
+                       } else {
+                               lastCompileSpec = new CompilerRun.Spec();
+                               lastCompileSpec.testSrcDirOffset = null;
+                               // srcs are in test base for old
+                               lastCompileSpec.addOptions(compileOptions);
+                               lastCompileSpec.addPaths(words);
+                               lastFile = new File(words[words.length - 1]);
+                               result.addChild(lastCompileSpec);
+                       }
+               }
+               if (null == lastCompileSpec) {
+                       throw new AbortException("expected sources at " + reader);
+               }
+
+               ArrayList exp = new ArrayList();
+               // !compile || noerrors || className {runOption..}
+               String first = words[0];
+               if ("!compile".equals(first)) {
+                       //result.className = words[0];
+                       //result.runOptions = new String[words.length-1];
+                       //System.arraycopy(words, 0, result.runOptions, 0, words.length-1);
+               } else if ("noerrors".equals(first)) {
+                       // className is null, but no errors expected
+                       // so compile succeeds but run not attempted
+                       //result.errors = Main.RA_ErrorLine;
+                       // result.runOptions = Main.RA_String;
+               } else if (isNumber(first) || (-1 != first.indexOf(":"))) {
+                       exp.addAll(makeMessages(IMessage.ERROR, words, 0, lastFile));
+               } else {
+                       String[] args = new String[words.length - 1];
+                       System.arraycopy(words, 0, args, 0, args.length);
+                       JavaRun.Spec spec = new JavaRun.Spec();
+                       spec.className = first;
+                       spec.addOptions(args);
+                       //XXXrun.runDir = sourceRoot;
+                       result.addChild(spec);
+               }
+
+               // optional: warnings, eclipse.warnings, eclipse.errors
+               // XXX unable to specify error in eclipse but not ajc
+               boolean gotErrors = false;
+               while (null
+                       != (line =
+                               r.read(
+                                       " errors, warnings, eclipse.warnings, eclipse.error",
+                                       false))) {
+                       words = LangUtil.split(line);
+                       first = words[0];
+                       if ("eclipse.warnings:".equals(first)) {
+                               if (usingEclipse) {
+                                       exp.addAll(
+                                               makeMessages(
+                                                       IMessage.WARNING,
+                                                       words,
+                                                       0,
+                                                       lastFile));
+                               }
+                       } else if ("eclipse.errors:".equals(first)) {
+                               if (usingEclipse) {
+                                       exp.addAll(
+                                               makeMessages(IMessage.ERROR, words, 0, lastFile));
+                               }
+                       } else if ("warnings:".equals(first)) {
+                               exp.addAll(
+                                       makeMessages(IMessage.WARNING, words, 0, lastFile));
+                       } else if (gotErrors) {
+                               exp.addAll(
+                                       makeMessages(IMessage.WARNING, words, 0, lastFile));
+                       } else {
+                               exp.addAll(
+                                       makeMessages(IMessage.ERROR, words, 0, lastFile));
+                               gotErrors = true;
+                       }
+               }
+               lastCompileSpec.addMessages(exp);
+
+               int endLine = reader.getLineNumber();
+               File sourceFile = reader.getFile();
+               ISourceLocation sl =
+                       new SourceLocation(sourceFile, startLine, endLine, 0);
+               result.setSourceLocation(sl);
+
+        if (clean) {
+            cleanup(result, reader);
+        }
+        return result;
+       }
+    
+    /** post-process result 
+     * - use file name as keyword
+     * - clip / for dir offsets
+     * - extract purejava keyword variants
+     * - extract bugID
+     * - convert test options to force-options
+     * - detect illegal xml characters
+     */
+    private void cleanup(AjcTest.Spec result, LineReader lineReader) {
+        LangUtil.throwIaxIfNull(result, "result");
+        LangUtil.throwIaxIfNull(lineReader, "lineReader");
+
+        File suiteFile = lineReader.getFile();
+        String name = suiteFile.getName();
+        if (!name.endsWith(".txt")) {
+            throw new Error("unexpected name: " + name);
+        }
+        result.addKeyword("from-" + name.substring(0,name.length()-4));
+
+        final String dir = result.testDirOffset;
+        if (dir.endsWith("/")) {
+            result.testDirOffset = dir.substring(0,dir.length()-1);
+        }
+
+        StringBuffer description = new StringBuffer(result.description);
+        if (strip(description, "PUREJAVA")) {
+            result.addKeyword("purejava");
+        }
+        if (strip(description, "PUREJAVE")) {
+            result.addKeyword("purejava");
+        }
+        if (strip(description, "[purejava]")) {
+            result.addKeyword("purejava");
+        }
+        String input = description.toString();
+        int loc = input.indexOf("PR#");
+        if (-1 != loc) {
+            String prefix = input.substring(0, loc).trim();
+            String pr = input.substring(loc+3, loc+6).trim();
+            String suffix = input.substring(loc+6).trim();
+            description.setLength(0);
+            description.append((prefix + " " + suffix).trim());
+            try {
+                result.setBugId(Integer.valueOf(pr).intValue());
+            } catch (NumberFormatException e) {
+                throw new Error("unable to convert " + pr + " for " + result
+                    + " at " + lineReader);
+            }                      
+        }
+        input = description.toString();
+        String error = null;
+        if (-1 != input.indexOf("&")) {
+            error = "char &";
+        } else if (-1 != input.indexOf("<")) {
+            error = "char <";
+        } else if (-1 != input.indexOf(">")) {
+            error = "char >";
+        } else if (-1 != input.indexOf("\"")) {
+            error = "char \"";
+        }
+        if (null != error) {
+            throw new Error(error + " in " + input + " at " + lineReader);
+        }
+        result.description = input;
+    
+        ArrayList newOptions = new ArrayList();
+        ArrayList optionsCopy = result.getOptionsList();
+        for (Iterator iter = optionsCopy.iterator(); iter.hasNext();) {
+                       String option = (String) iter.next();
+                       if (option.startsWith("-")) {
+                newOptions.add("!" + option.substring(1));
+            } else {
+                throw new Error("non-flag option? " + option);
+            }
+               }
+        result.setOptionsArray((String[]) newOptions.toArray(new String[0]));
+    }
+    
+    private boolean strip(StringBuffer sb, String infix) {
+        String input = sb.toString();
+        int loc = input.indexOf(infix);
+        if (-1 != loc) {
+            String prefix = input.substring(0, loc);
+            String suffix = input.substring(loc+infix.length());
+            input = (prefix.trim() + " " + suffix.trim()).trim();
+            sb.setLength(0);
+            sb.append(input);
+            return true;
+        }
+        return false;
+    }
+
+       /**
+        * Generate list of expected messages of this kind.
+        * @param kind any non-null kind, but s.b. IMessage.WARNING or ERROR
+        * @param words
+        * @param start index in words where to start
+        * @param lastFile default file for source location if the input does not specify
+        * @return List
+        */
+       private List makeMessages(// XXX weak - also support expected exceptions, etc.
+       Kind kind, String[] words, int start, File lastFile) {
+               ArrayList result = new ArrayList();
+               for (int i = start; i < words.length; i++) {
+                       ISourceLocation sl =
+                               BridgeUtil.makeSourceLocation(words[i], lastFile);
+                       if (null == sl) { // XXX signalling during make
+                               // System.err.println(...);
+                               //MessageUtil.debug(handler, "not a source location: " + words[i]);
+                       } else {
+                               String text =
+                                       (("" + sl.getLine()).equals(words[i]) ? "" : words[i]);
+                               result.add(new Message(text, kind, null, sl));
+                       }
+               }
+               return (0 == result.size() ? Collections.EMPTY_LIST : result);
+       }
+
+       /** 
+        * Read suite spec from a flat .txt file.
+        * @throws AbortException on failure
+        * @return AjcTest.Suite.Spec with any AjcTest.Spec as children
+        */ 
+       public AjcTest.Suite.Spec readSuite(File suiteFile) {
+               LangUtil.throwIaxIfNull(suiteFile, "suiteFile");
+               if (!suiteFile.isAbsolute()) {
+                       suiteFile = suiteFile.getAbsoluteFile();
+               }
+        final AjcTest.Suite.Spec result = new AjcTest.Suite.Spec();
+        result.setSuiteDirFile(suiteFile.getParentFile());
+               ObjectChecker collector = new StandardObjectChecker(IRunSpec.class) {
+                       public boolean doIsValid(Object o) {
+                result.addChild((IRunSpec) o);
+                               return true;
+                       }
+               };
+               boolean abortOnError = true;
+               try {
+                       READER.readNodes(
+                               suiteFile,
+                               collector,
+                               abortOnError,
+                               System.err);
+               } catch (IOException e) {
+                       IMessage m = Message.fail("reading " + suiteFile, e);
+                       throw new AbortException(m);
+               }
+        
+               return result;
+       }
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/Globals.java b/testing/src/org/aspectj/testing/harness/bridge/Globals.java
new file mode 100644 (file)
index 0000000..7c4f524
--- /dev/null
@@ -0,0 +1,94 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+ package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+
+/**
+ */
+public class Globals {
+    /** name/key of the System property to set to define library dir */
+    public static final String LIBDIR_NAME = "harness.libdir";
+
+    /** name/key of the System property to set to define J2SE_HOME dir */
+    public static final String J2SE14_RTJAR_NAME = "j2se14.rtjar";
+
+    /** assumed relative location of a library with required jars */
+    public static final String LIBDIR = getSystemProperty(LIBDIR_NAME, "../lib/test");
+
+    /** Path to J2SE_HOME */
+    public static final File J2SE14_RTJAR;
+
+    /** array of parameter types for main(String[]) */
+    public static final Class[] MAIN_PARM_TYPES = new Class[] {String[].class};    
+    public static final String S_testingclient_jar  = LIBDIR + "/testing-client.jar";
+    public static final String S_aspectjrt_jar      = LIBDIR + "/aspectjrt.jar";
+    public static final String S_bridge_jar         = LIBDIR + "/bridge.jar";
+    public static final File F_testingclient_jar    = new File(S_testingclient_jar);
+    public static final File F_aspectjrt_jar        = new File(S_aspectjrt_jar);
+    public static final File F_bridge_jar           = new File(S_bridge_jar);
+    public static final boolean globalsValid;
+    
+    static {
+        File j2seJar = null;
+        String path = getSystemProperty(J2SE14_RTJAR_NAME, "c:/home/apps/jdk14");
+        File j2seHome = new File(path);
+        if (j2seHome.exists() && j2seHome.isDirectory()) {
+            File rtjar = new File(j2seHome.getAbsolutePath() + "/jre/lib/rt.jar");
+            if (rtjar.canRead() && rtjar.isFile()) {
+                j2seJar = rtjar;
+            }
+        }
+        J2SE14_RTJAR = j2seJar;
+       globalsValid = 
+               (FileUtil.canReadFile(F_testingclient_jar)
+                 && FileUtil.canReadFile(F_aspectjrt_jar)
+                 && FileUtil.canReadFile(F_bridge_jar)
+              && FileUtil.canReadFile(J2SE14_RTJAR)
+            );
+    }
+
+       /**
+        * 
+        * @return null if not found, or 
+     *          String with class path for compiler to load J2SE 1.4 classes from.
+        */
+       public static String get14Bootclasspath() {
+        return null;
+       }
+
+       /**
+        * Get System property completely safely.
+        * @param propertyName the String name of the property to get
+        * @param defaultValue the String default value to return value is null or empty
+        * @return String value or defaultValue if not available.
+        */
+       private static String getSystemProperty(
+               String propertyName,
+               String defaultValue) {
+        String result = defaultValue;
+        try {
+            String value = System.getProperty(propertyName);
+            if (!LangUtil.isEmpty(value)) {
+                result = value;
+            }
+        } catch (Throwable t) {}
+        return result;      
+       }
+
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/IAjcRun.java b/testing/src/org/aspectj/testing/harness/bridge/IAjcRun.java
new file mode 100644 (file)
index 0000000..061fde4
--- /dev/null
@@ -0,0 +1,44 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.testing.run.IRun;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.xml.XMLWriter;
+
+// XXX candidate to be subsumed in class/constructors, since inner spec does setup
+//     at the same time it constructs the run.
+public interface IAjcRun extends IRun {
+    boolean setupAjcRun(Sandbox sandbox, Validator validator);
+    // XXX add for result eval? ArrayList getExpectedMessages();
+    
+    /** this IAjcRun does nothing, returning true always */
+    public static final IAjcRun NULLRUN = new IAjcRun() {
+        public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
+            return true;
+        }
+        public boolean run(IRunStatus status) {
+            if (!status.started()) {
+                status.start();
+            }
+            status.finish(IRunStatus.PASS);
+            return true;
+        }
+
+        public void writeXml(XMLWriter out) {
+            throw new UnsupportedOperationException("unimplemented");
+        }
+        public String toString() { return "IAjcRun.NULLRUN"; }
+    };
+    
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/IRunSpec.java b/testing/src/org/aspectj/testing/harness/bridge/IRunSpec.java
new file mode 100644 (file)
index 0000000..7cb0076
--- /dev/null
@@ -0,0 +1,24 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.xml.IXmlWritable;
+
+/**
+ * A run spec can make a run iterator and write itself as XML.
+ */
+public interface IRunSpec extends IXmlWritable {
+    IRunIterator makeRunIterator(Sandbox sandbox, Validator validator);
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/IncCompilerRun.java b/testing/src/org/aspectj/testing/harness/bridge/IncCompilerRun.java
new file mode 100644 (file)
index 0000000..efdd089
--- /dev/null
@@ -0,0 +1,444 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.ICommand;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.WrappedRunIterator;
+import org.aspectj.testing.xml.SoftMessage;
+import org.aspectj.testing.xml.XMLWriter;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An incremental compiler run takes an existing compiler commmand
+ * from the sandbox, updates the staging directory, and recompiles.
+ * The staging directory is updated by prefix/suffix rules applied
+ * to files found below Sandbox.testBaseSrcDir.
+ * Files with suffix .{tag}.java are owned by this run
+ * and are copied to the staging directory 
+ * unless they are prefixed "delete.", in which case the
+ * corresponding file is deleted.  Any "owned" file is passed to
+ * the compiler as the list of changed files.
+ * The files entry contains the expected files recompiled. XXX underinclusive 
+ * XXX prefer messages for expected files?
+ * XXX later: also support specified paths, etc.
+ */
+public class IncCompilerRun implements IAjcRun {
+
+       final Spec spec; // nonfinal later to make re-runnable
+    Sandbox sandbox;
+    
+       /**
+        * @param handler must not be null, but may be reused in the same thread
+        */
+       public IncCompilerRun(Spec spec) {
+               LangUtil.throwIaxIfNull(spec, "spec");
+               this.spec = spec;
+       }
+
+       /** 
+        * Initialize this from the sandbox, using compiler and changedFiles.
+        * @param sandbox the Sandbox setup for this test, including copying
+        *         any changed files, etc.
+        * @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
+        * @throws AbortException containing IOException or IllegalArgumentException
+        *          if the staging operations fail
+        */
+       public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
+               LangUtil.throwIaxIfNull(validator, "validator");
+               if (!validator.nullcheck(sandbox, "sandbox")
+                       || !validator.nullcheck(spec, "spec")
+                       || !validator.nullcheck(spec.tag, "fileSuffix")) {
+                       return false;
+               }
+               File srcDir = sandbox.getTestBaseSrcDir(this);
+               File destDir = sandbox.stagingDir;
+               if (!validator.canReadDir(srcDir, "testBaseSrcDir")
+                       || !validator.canReadDir(destDir, "stagingDir")) {
+                       return false;
+               }
+
+        this.sandbox = sandbox;
+        return doStaging(validator);
+       }
+        
+    /** 
+     * Handle copying and deleting of files per tag.
+     * This returns false unless some file was copied or deleted successfully
+     * and there were no failures copying or deleting files.
+     * @return true if staging completed successfully 
+     */
+    boolean doStaging(final Validator validator) {
+        boolean result = false;
+        try {
+            //ArrayList changed = new ArrayList();
+            final String toSuffix = ".java";
+            final String fromSuffix = "." + spec.tag + toSuffix;
+            // copy our tagged generation of files to the staging directory,
+            // deleting any with ChangedFilesCollector.DELETE_SUFFIX
+            class intHolder {
+                int numCopies;
+                int numDeletes;
+                int numFails;
+            }
+            final intHolder holder = new intHolder();
+            FileFilter deleteOrCount = new FileFilter() {
+                final String clip = ".delete" + toSuffix;
+                /** do copy unless file should be deleted */
+                               public boolean accept(File file) {
+                    boolean doCopy = true;
+                    String path = file.getAbsolutePath();
+                    if (!path.endsWith(clip)) {
+                        holder.numCopies++;
+                    } else {
+                        doCopy = false;
+                        path = path.substring(0, path.length()-clip.length()) + toSuffix;
+                        File toDelete = new File(path);
+                        if (toDelete.delete()) {
+                            holder.numDeletes++;
+                        } else {
+                            validator.fail("unable to delete file: " + path);
+                            holder.numFails++;
+                        }
+                    }
+                                       return doCopy;
+                               }
+
+            };
+            File srcDir = sandbox.getTestBaseSrcDir(this);
+            File destDir = sandbox.stagingDir;
+            FileUtil.copyDir(srcDir, destDir, fromSuffix, toSuffix, deleteOrCount);
+            if ((0 == holder.numCopies) && (0 == holder.numDeletes)) {
+                validator.fail("no files changed??");
+            } else {
+                result = (0 == holder.numFails);
+            }
+        } catch (NullPointerException npe) {
+            validator.fail("staging - input", npe);
+        } catch (IOException e) {
+            validator.fail("staging - operations", e);
+        }
+        return result;
+    }
+    
+       /**
+        * @see org.aspectj.testing.run.IRun#run(IRunStatus)
+        */
+       public boolean run(IRunStatus status) {
+
+               ICommand compiler = sandbox.getCommand(this);
+               if (null == compiler) {
+                       MessageUtil.abort(status, "null compiler");
+               }
+        
+//        // This is a list of expected classes (in File-normal form
+//        // relative to base class/src dir, without .class suffix
+//        // -- like "org/aspectj/tools/ajc/Main")
+//        // A preliminary list is generated in doStaging.
+//        ArrayList expectedClasses = doStaging(status);
+//        if (null == expectedClasses) {
+//            return false;
+//        }
+//
+//        // now add any (additional) expected-class entries listed in the spec
+//        // normalize to a similar file path (and do info messages for redundancies).  
+//
+//        List alsoChanged = spec.getPathsAsFile(sandbox.stagingDir);
+//        for (Iterator iter = alsoChanged.iterator(); iter.hasNext();) {
+//                     File f = (File) iter.next();
+//            
+//            if (expectedClasses.contains(f)) {
+//                // XXX remove old comment changed.contains() works b/c getPathsAsFile producing both File
+//                // normalizes the paths, and File.equals(..) compares these lexically
+//                String s = "specification of changed file redundant with tagged file: ";
+//                MessageUtil.info(status, s + f);
+//            } else {
+//                expectedClasses.add(f);
+//            }                        
+//             }
+//        
+//        // now can create handler, use it for reporting
+//        List errors = spec.getMessages(IMessage.ERROR);
+//        List warnings = spec.getMessages(IMessage.WARNING);
+//        AjcMessageHandler handler = new AjcMessageHandler(errors, warnings, expectedClasses);
+        
+        // same DirChanges handling for JavaRun, CompilerRun, IncCompilerRun 
+        // XXX around advice or template method/class
+        DirChanges dirChanges = null;
+        if (!LangUtil.isEmpty(spec.dirChanges)) {
+            LangUtil.throwIaxIfFalse(1 == spec.dirChanges.size(), "expecting only 1 dirChanges");
+            dirChanges = new DirChanges((DirChanges.Spec) spec.dirChanges.get(0));
+            if (!dirChanges.start(status, sandbox.classesDir)) {
+                return false; // setup failed
+            }
+        }
+        List errors = spec.getMessages(IMessage.ERROR);
+        List warnings = spec.getMessages(IMessage.WARNING);
+        List expectRecompiled = Collections.EMPTY_LIST;
+        AjcMessageHandler handler = new AjcMessageHandler(errors, warnings, expectRecompiled);
+        boolean handlerResult = false;
+        boolean commandResult = false;
+        boolean result = false;
+        boolean report = false;
+        try {
+            handler.init();
+            final long startTime = System.currentTimeMillis();
+            commandResult = compiler.repeatCommand(handler);
+            // XXX disabled LangUtil.throwIaxIfNotAllAssignable(actualRecompiled, File.class, "recompiled");
+            report = true;
+            // handler does not verify sandbox...
+            handlerResult = handler.passed();
+            if (!handlerResult) {
+                result = false;
+            } else {
+                result = (commandResult == handler.expectingCommandTrue());
+                if (! result) {
+                    String m = commandResult 
+                        ? "incremental compile command did not return false as expected"
+                        : "incremental compile command returned false unexpectedly";                        
+                    MessageUtil.fail(status, m);
+                } else if (null != dirChanges) {
+                    result = dirChanges.end(status, sandbox.testBaseDir);
+                }
+            }
+        } finally {
+            if (!result || spec.runtime.isVerbose()) { // more debugging context in case of failure
+                MessageUtil.info(handler, "spec: " + spec.toLongString());
+                MessageUtil.info(handler, "sandbox: " + sandbox);
+                String[] classes = FileUtil.listFiles(sandbox.classesDir);
+                MessageUtil.info(handler, "sandbox.classes: " + Arrays.asList(classes));
+            }
+            // XXX weak - actual messages not reported in real-time, no fast-fail
+            if (report) {
+                handler.report(status); 
+            }
+        }             
+        return result;
+       }
+
+       private boolean hasFile(ArrayList changed, File f) {
+               return changed.contains(f); // d
+       }
+
+
+       public String toString() {
+      return "" + spec; 
+      //               return "IncCompilerRun(" + spec + ")"; // XXX
+       }
+
+//     /** @see IXmlWritable#writeXml(XMLWriter) */
+//     public void writeXml(XMLWriter out) {
+//        String elementName = "inc-compile";
+//        String tagAttr = out.makeAttribute("tag", spec.tag);
+//        List messages = spec.getMessages();
+//        int nMessages = messages.size();
+//        if (0 == nMessages) {
+//            out.printElement(elementName, tagAttr);
+//        } else {
+//            out.startElement(elementName, tagAttr, true);
+//            SoftMessage.writeXml(out, messages);
+//            out.endElement(elementName);
+//        }        
+//     }
+//
+       /** 
+     * initializer/factory for IncCompilerRun.
+     */
+       public static class Spec extends AbstractRunSpec {
+        public static final String XMLNAME = "inc-compile";
+
+        protected ArrayList classesAdded;
+        protected ArrayList classesRemoved;
+        protected ArrayList classesUpdated;
+
+        /**
+         * skip description, skip sourceLocation, 
+         * do keywords, skip options, do paths as classes, do comment,
+         * do dirChanges, do messages but skip children. 
+         */
+        private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
+                "", "", null, "", "classes", null, false, false, true);
+                
+               /** identifies files this run owns, so {name}.{tag}.java maps to {name}.java */
+               String tag;
+
+               public Spec() {
+            super(XMLNAME);
+                       setStaging(true);
+            classesAdded = new ArrayList();
+            classesRemoved = new ArrayList();
+            classesUpdated = new ArrayList();
+               }
+        
+               public void setTag(String input) {
+                       tag = input;
+               }
+        public String toString() {
+            return "IncCompile.Spec(" + tag + ", " + super.toString() + ")";
+        }
+        
+        /** override to set dirToken to Sandbox.CLASSES and default suffix to ".class" */
+        public void addDirChanges(DirChanges.Spec spec) { // XXX copy/paste of CompilerRun.Spec...
+            if (null == spec) {
+                return;
+            }
+            spec.setDirToken(Sandbox.CLASSES_DIR);
+            spec.setDefaultSuffix(".class");
+            super.addDirChanges(spec);
+        }
+        
+        /** @return a IncCompilerRun with this as spec if setup completes successfully. */
+        public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
+            IncCompilerRun run = new IncCompilerRun(this);
+            if (run.setupAjcRun(sandbox, validator)) {
+                // XXX need name
+                return new WrappedRunIterator(this, run);
+            }
+            return null;
+        }
+        
+       /** 
+         * Write this out as a compile element as defined in
+         * AjcSpecXmlReader.DOCTYPE.
+         * @see AjcSpecXmlReader#DOCTYPE 
+         * @see IXmlWritable#writeXml(XMLWriter) 
+         */
+        public void writeXml(XMLWriter out) {
+            String attr = out.makeAttribute("tag", tag);
+            out.startElement(xmlElementName, attr, false);
+            super.writeAttributes(out);
+            out.endAttributes();
+            if (!LangUtil.isEmpty(dirChanges)) {
+                DirChanges.Spec.writeXml(out, dirChanges);
+            }
+            List messages = getMessages();
+            if (0 < messages.size()) {
+                SoftMessage.writeXml(out, messages);
+            }
+            out.endElement(xmlElementName);
+        }
+        
+        public void setClassesAdded(String items) {
+            addItems(classesAdded, items);
+        }
+        
+        public void setClassesUpdated(String items) {
+            addItems(classesUpdated, items);
+        }
+        
+        public void setClassesRemoved(String items) {
+            addItems(classesRemoved, items);
+        }
+        
+        private void addItems(ArrayList list, String items) {
+            if (null != items) {
+                String[] classes = XMLWriter.unflattenList(items);
+                if (!LangUtil.isEmpty(classes)) {
+                    for (int i = 0; i < classes.length; i++) {
+                        if (!LangUtil.isEmpty(classes[i])) {
+                            list.add(classes[i]);
+                        }
+                    }
+                }
+            }    
+        }
+       } // class IncCompilerRun.Spec
+}
+//   // XXX replaced with method-local class - revisit if useful
+//    
+//     /** 
+//      * This class collects the list of all changed files and
+//      * deletes the corresponding file for those prefixed "delete."
+//      */
+//     static class ChangedFilesCollector implements FileFilter {
+//        static final String DELETE_SUFFIX = ".delete.java";
+//        static final String REPLACE_SUFFIX = ".java";
+//             final ArrayList changed;
+//             final Validator validator;
+//        /** need this to generate paths by clipping */
+//        final File destDir;
+//        
+//             /** @param changed the sink for all files changed (full paths) */
+//             public ChangedFilesCollector(ArrayList changed, File destDir, Validator validator) {
+//                     LangUtil.throwIaxIfNull(validator, "ChangedFilesCollector - handler");
+//                     this.changed = changed;
+//                     this.validator = validator;
+//            this.destDir = destDir;
+//        }
+//
+//             /**
+//              * This converts the input File to normal String path form 
+//         * (without any source suffix) and adds it to the list changed.
+//         * If the name of the file is suffixed ".delete..", then
+//              * delete the corresponding file, and return false (no copy).  
+//         * Return true otherwise (copy file).  
+//         * @see java.io.FileFilter#accept(File)
+//              */
+//             public boolean accept(File file) {
+//            final String aname = file.getAbsolutePath();
+//            String name = file.getName();
+//            boolean doCopy = true;
+//            boolean failed = false;
+//            if (name.endsWith(DELETE_SUFFIX)) {
+//                name = name.substring(0,name.length()-DELETE_SUFFIX.length());
+//                file = file.getParentFile();
+//                file = new File(file, name + REPLACE_SUFFIX);
+//                if (!file.canWrite()) {
+//                    validator.fail("file to delete is not writable: " + file);
+//                    failed = true;
+//                } else if (!file.delete()) {
+//                    validator.fail("unable to delete file: " + file);
+//                    failed = true;
+//                }
+//                doCopy = false;
+//            }
+//            if (!failed && doCopy) {
+//                int clip = FileUtil.sourceSuffixLength(file);
+//                if (-1 != clip) {
+//                    name.substring(0, name.length()-clip);
+//                }
+//                if (null != destDir) {
+//                    String path = destDir.getPath();
+//                    if (!LangUtil.isEmpty(path)) {
+//                        // XXX incomplete
+//                        if (name.startsWith(path)) {
+//                        } else {
+//                            int loc = name.lastIndexOf(path);
+//                            if (-1 == loc) { // sigh
+//                                
+//                            } else {
+//                            
+//                            }
+//                        }
+//                    }
+//                }
+//                name = FileUtil.weakNormalize(name);
+//                changed.add(file);
+//            }
+//            return doCopy;
+//             }
+//     };
+
diff --git a/testing/src/org/aspectj/testing/harness/bridge/JavaRun.java b/testing/src/org/aspectj/testing/harness/bridge/JavaRun.java
new file mode 100644 (file)
index 0000000..5d7dd70
--- /dev/null
@@ -0,0 +1,298 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.Tester;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.WrappedRunIterator;
+import org.aspectj.testing.util.TestClassLoader;
+import org.aspectj.testing.xml.SoftMessage;
+import org.aspectj.testing.xml.XMLWriter;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Run a class in this VM using reflection.
+ */
+public class JavaRun implements IAjcRun {
+    Spec spec;
+       private Sandbox sandbox;   
+
+    /** programmatic initialization per spec */
+    public JavaRun(Spec spec) {
+        this.spec = spec;
+    }
+    // XXX init(Spec)
+    
+    /**
+     * This checks the spec for a class name
+     * and checks the sandbox for a readable test source directory,
+     * a writable run dir, and (non-null, possibly-empty) lists
+     * of readable classpath dirs and jars.
+        * @return true if all checks pass
+     * @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
+        */
+       public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
+               this.sandbox = sandbox;        
+               return (validator.nullcheck(spec.className, "class name")
+            && validator.nullcheck(sandbox, "sandbox")
+            && validator.canReadDir(sandbox.getTestBaseSrcDir(this), "testBaseSrc dir")
+            && validator.canWriteDir(sandbox.runDir, "run dir")
+            && validator.canReadFiles(sandbox.getClasspathJars(true, this), "classpath jars")
+            && validator.canReadDirs(sandbox.getClasspathDirectories(true, this), "classpath dirs")
+            );            
+        
+       }
+    
+    /** caller must record any exceptions */
+    public boolean run(IRunStatus status)
+        throws IllegalAccessException,
+                InvocationTargetException,
+                ClassNotFoundException,
+                NoSuchMethodException {
+
+        boolean completedNormally = false;
+        Class targetClass = null;
+        if (!LangUtil.isEmpty(spec.dirChanges)) {
+            MessageUtil.info(status, "XXX dirChanges not implemented in JavaRun");
+        }
+        try {
+            final boolean readable = true;
+            File[] libs = sandbox.getClasspathJars(readable, this);
+            URL[] urls = FileUtil.getFileURLs(libs);
+            File[] dirs = sandbox.getClasspathDirectories(readable, this);
+            ClassLoader loader = new TestClassLoader(urls, dirs);
+            // make the following load test optional
+            // Class testAspect = loader.loadClass("org.aspectj.lang.JoinPoint");
+            targetClass = loader.loadClass(spec.className);
+            Method main = targetClass.getMethod("main", Globals.MAIN_PARM_TYPES);
+            setupTester(sandbox.getTestBaseSrcDir(this), loader, status);
+            main.invoke(null, new Object[] { spec.getOptionsArray() });
+            completedNormally = true;
+        } catch (ClassNotFoundException e) {
+            String[] classes = FileUtil.listFiles(sandbox.classesDir);
+            MessageUtil.info(status, "sandbox.classes: " + Arrays.asList(classes));
+            MessageUtil.fail(status, null, e);
+        } finally {
+            if (!completedNormally) {
+                MessageUtil.info(status, spec.toLongString());
+                MessageUtil.info(status, "targetClass: " + targetClass);
+                MessageUtil.info(status, "" + sandbox);
+            }
+        }
+        return completedNormally;
+    }
+
+    /**
+     * Clear (static) testing state and setup base directory,
+     * unless spec.skipTesting.
+     * @return null if successful, error message otherwise
+     */
+    protected void setupTester(File baseDir, ClassLoader loader, IMessageHandler handler) {
+        if (null == loader) {
+            setupTester(baseDir, handler);
+            return;
+        }
+        File baseDirSet = null;
+        try {
+            if (!spec.skipTester) { 
+                Class tc = loader.loadClass("org.aspectj.testing.Tester");
+                // Tester.clear();
+                Method m = tc.getMethod("clear", new Class[0]);
+                m.invoke(null, new Object[0]);             
+                // Tester.setMessageHandler(handler);
+                m = tc.getMethod("setMessageHandler", new Class[] {IMessageHandler.class});
+                m.invoke(null, new Object[] { handler});             
+                
+                //Tester.setBASEDIR(baseDir);
+                m = tc.getMethod("setBASEDIR", new Class[] {File.class});
+                m.invoke(null, new Object[] { baseDir});             
+
+                //baseDirSet = Tester.getBASEDIR();
+                m = tc.getMethod("getBASEDIR", new Class[0]);
+                baseDirSet = (File) m.invoke(null, new Object[0]);             
+                
+                if (!baseDirSet.equals(baseDir)) {
+                    String l = "AjcScript.setupTester() setting "
+                             + baseDir + " returned " + baseDirSet;
+                    MessageUtil.debug(handler, l);
+                }
+            }
+        } catch (Throwable t) {
+            MessageUtil.abort(handler, "baseDir=" + baseDir, t);
+        }
+    }
+
+    /**
+     * Clear (static) testing state and setup base directory,
+     * unless spec.skipTesting.
+     * This implementation assumes that Tester is defined for the
+     * same class loader as this class.
+     * @return null if successful, error message otherwise
+     */
+    protected void setupTester(File baseDir, IMessageHandler handler) {
+        File baseDirSet = null;
+        try {
+            if (!spec.skipTester) {                
+                Tester.clear();
+                Tester.setMessageHandler(handler);
+                Tester.setBASEDIR(baseDir);
+                baseDirSet = Tester.getBASEDIR();
+                if (!baseDirSet.equals(baseDir)) {
+                    String l = "AjcScript.setupTester() setting "
+                             + baseDir + " returned " + baseDirSet;
+                    MessageUtil.debug(handler, l);
+                }
+            }
+        } catch (Throwable t) {
+            MessageUtil.abort(handler, "baseDir=" + baseDir, t);
+        }
+    }
+       public String toString() {
+        return "JavaRun(" + spec + ")";
+       }
+        
+    /** 
+     * Initializer/factory for JavaRun.
+     * The classpath is not here but precalculated in the Sandbox. XXX libs?
+     */
+    public static class Spec extends AbstractRunSpec {
+        public static final String XMLNAME = "run";
+        /**
+         * skip description, skip sourceLocation, 
+         * do keywords, do options, skip paths, do comment, 
+         * do dirChanges, do messages but skip children. 
+         */
+        private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
+                "", "", null, null, "", null, false, false, true);
+                
+        /** fully-qualified name of the class to run */
+        protected String className;
+        
+        /** minimum required version of Java, if any */
+        protected String javaVersion;
+        
+        /** if true, skip Tester setup (e.g., if Tester n/a) */
+        protected boolean skipTester;
+        
+        public Spec() {
+            super(XMLNAME);
+            setXMLNames(NAMES);
+        }
+        
+        /**
+         * @param version "1.1", "1.2", "1.3", "1.4"
+         * @throws IllegalArgumentException if version is not recognized
+         */
+        public void setJavaVersion(String version) {
+            LangUtil.supportsJava(version);
+            this.javaVersion = version;
+        }
+        
+        /** @className fully-qualified name of the class to run */
+        public void setClassName(String className) {
+            this.className = className;
+        }
+        
+        /** @param skip if true, then do not set up Tester */
+        public void setSkipTester(boolean skip) {
+            skipTester = skip;
+        }
+        
+        /** override to set dirToken to Sandbox.RUN_DIR */
+        public void addDirChanges(DirChanges.Spec spec) {
+            if (null == spec) {
+                return;
+            }
+            spec.setDirToken(Sandbox.RUN_DIR);
+            super.addDirChanges(spec);
+        }
+
+        /** @return a JavaRun with this as spec if setup completes successfully. */
+        public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
+            JavaRun run = new JavaRun(this);
+            if (run.setupAjcRun(sandbox, validator)) {
+                // XXX need name for JavaRun
+                return new WrappedRunIterator(this, run);
+            }
+            return null;
+        }
+
+        /** 
+         * Write this out as a run element as defined in
+         * AjcSpecXmlReader.DOCTYPE.
+         * @see AjcSpecXmlReader#DOCTYPE 
+         * @see IXmlWritable#writeXml(XMLWriter) 
+         */
+        public void writeXml(XMLWriter out) {
+            String attr = out.makeAttribute("class", className);
+            out.startElement(xmlElementName, attr, false);
+            if (skipTester) {
+                out.printAttribute("skipTester", "true");
+            }
+            if (null != javaVersion) {
+                out.printAttribute("vm", javaVersion);
+            }
+            super.writeAttributes(out);
+            out.endAttributes();
+            if (!LangUtil.isEmpty(dirChanges)) {
+                DirChanges.Spec.writeXml(out, dirChanges);
+            }
+            List messages = getMessages();
+            if (0 < messages.size()) {
+                SoftMessage.writeXml(out, messages);
+            }
+            out.endElement(xmlElementName);
+        }
+        public String toLongString() {
+            return toString() + "[" + super.toLongString() + "]";        
+        }
+        
+        public String toString() {
+            if (skipTester) {
+                return "JavaRun(" + className + ", skipTester)";
+            } else {
+                return "JavaRun(" + className + ")";
+            }
+        }
+        
+        /**
+         * This implementation skips if:
+         * <ul>
+         * <li>current VM is not at least any specified javaVersion </li>
+         * </ul>
+         * @return false if this wants to be skipped, true otherwise
+         */
+        protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
+            if (!super.doAdoptParentValues(parentRuntime, handler)) {
+                return false;
+            }
+            if ((null != javaVersion) && (!LangUtil.supportsJava(javaVersion))) {
+                skipMessage(handler, "requires Java version " + javaVersion);
+                return false;
+            }
+            return true;
+        }  
+     }
+}
+       
diff --git a/testing/src/org/aspectj/testing/harness/bridge/RunSpecIterator.java b/testing/src/org/aspectj/testing/harness/bridge/RunSpecIterator.java
new file mode 100644 (file)
index 0000000..ebc4c14
--- /dev/null
@@ -0,0 +1,256 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.Message;
+import org.aspectj.testing.run.IRun;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.run.Runner;
+import org.aspectj.testing.run.WrappedRunIterator;
+import org.aspectj.util.LangUtil;
+
+import java.util.ArrayList;
+
+
+/** 
+ * This wraps an AbstractRunSpec, which has children that
+ * return IRunIterator, the results of which we return 
+ * from nextRun(..)  
+ * We extract global options from the AbstractRunSpec options 
+ * and set the global options in the AbstractRunSpec,
+ * which is responsible for setting them in any children
+ * during makeRun(..).
+ */
+public class RunSpecIterator implements IRunIterator  {
+    /*
+     * When constructed, this gets its own spec
+     * and a sandbox to be used for making all children.
+     * In nextRun() this uses the spec's child specs
+     * and the sandbox to create the next child run iterator.
+     * This returns all the run provided by that child iterator
+     * before going to the next child.
+     */
+    /** spec for this test */
+    public final AbstractRunSpec spec; // XXX reconsider public after debugging
+    
+     /** current sandbox by default shared by all children */    
+    Sandbox sandbox;
+    
+    /** keep our copy to avoid recopying */
+    ArrayList childSpecs;
+    
+    /** index into child specs of next run */
+    int specIndex;
+    
+    /** child creation until the start of each run */
+    final Validator validator;
+    
+    /** current child iterator */
+    IRunIterator childIterator;
+    
+    final boolean haltOnFailure;
+
+       private int numIncomplete;
+
+       private final IMessage.Kind failureKind;
+
+       private boolean didCleanup;
+    
+    /**
+     * Create a RunSpecIterator.
+     * Failure messages are of type IMessage.ABORT if abortOnFailure is true,
+     * or IMessage.ERROR otherwise.
+     * @param spec the AbstractRunSpec whose children we iterate - not null
+     * @param sandbox the default Sandbox to use for children to make runs - may be null
+     * @param haltOnFailure if true, stop after any failure in providing runs
+     */
+    public RunSpecIterator(
+        AbstractRunSpec spec, 
+        Sandbox sandbox,
+        Validator validator, 
+        boolean haltOnFailure) { 
+        this(spec, sandbox, validator, haltOnFailure, 
+            (haltOnFailure ? IMessage.ABORT : IMessage.ERROR));
+    }
+    
+    /**
+     * Create a RunSpecIterator, specifying any failure message kind.
+     * @param spec the AbstractRunSpec whose children we iterate - not null
+     * @param sandbox the default Sandbox to use for children to make runs - may be null
+     * @param haltOnFailure if true, stop after any failure in providing runs
+     * @param failureKind the IMessage.Kind for any failure messages - if null, no messages sent
+     */
+    public RunSpecIterator(
+        AbstractRunSpec spec, 
+        Sandbox sandbox,
+        Validator validator, 
+        boolean haltOnFailure,
+        IMessage.Kind failureKind) { 
+        LangUtil.throwIaxIfNull(spec, "spec");
+        LangUtil.throwIaxIfNull(sandbox, "sandbox");
+        LangUtil.throwIaxIfNull(validator, "validator");
+        this.sandbox = sandbox;
+        this.spec = spec;
+        this.validator = validator;
+        this.haltOnFailure = haltOnFailure;
+        this.failureKind = failureKind;
+        reset();
+    }
+    
+    /**
+     * @return value set on construction for abortOnError
+        * @see org.aspectj.testing.run.IRunIterator#abortOnFailure()
+        */
+       public boolean abortOnFailure() {
+        return haltOnFailure;
+       }
+
+    /** reset to start at the beginning of the child specs. */
+    public void reset() { 
+        specIndex = 0;
+        childSpecs = spec.getWorkingChildren();
+        childIterator = null;
+        numIncomplete = 0;
+    }
+    
+    /** @return int number of child run attempts that did not produce IRun */
+    public int getNumIncomplete() {
+        return numIncomplete;
+    }
+    
+    /**
+        * @see org.aspectj.testing.run.IRunIterator#hasNextRun()
+        */
+    public boolean hasNextRun() {
+       return ((specIndex < childSpecs.size())
+                    || ((null != childIterator) 
+                        && childIterator.hasNextRun()));
+    }
+
+    /**
+     * Get the next child IRunIterator as an IRun.
+     * In case of failure to get the next child, 
+     * numIncomplete is incremented, and
+     * a message of type failureKind is passed to the handler 
+     * (if failureKind was defined in the contructor).
+     * @return next child IRunIterator wrapped as an IRun 
+     */
+    public IRun nextRun(final IMessageHandler handler, Runner runner) {
+       validator.pushHandler(handler);
+               try {
+               IRun result = null;
+               IRunSpec childSpec = null;
+               String error = null;
+               String specLabel = "getting run for child of \"" + spec + "\" ";
+               while ((null == result) && hasNextRun() && (null == error)) {
+                   if (null == childIterator) {
+                       childSpec = (IRunSpec) childSpecs.get(specIndex++);
+                       if (null == childSpec) {
+                           error = "unexpected - no more child specs at " + --specIndex;
+                       } else {        
+                           Sandbox sandbox = makeSandbox(childSpec, validator);
+                           if (null == sandbox) {
+                               error = "unable to make sandbox for \"" + childSpec + "\"";
+                               childIterator = null;
+                           } else {      
+                               IRunIterator iter = childSpec.makeRunIterator(sandbox, validator);
+                               if (null == iter) {
+                                   // client should read reason why from validator
+                                   error = "child \"" + childSpec + "\".makeRunIterator(..) returned null";
+                               } else {
+                                   // hoist: result not wrapped but single IRun
+                                   if ((iter instanceof WrappedRunIterator)) {
+                                       if (!iter.hasNextRun()) {
+                                           error = "child \"" + childSpec + "\".hasNextRun()"
+                                               + " is not true - should be exactly one run";
+                                       } else {
+                                           result = iter.nextRun(handler, runner);
+                                           if (null == result) {
+                                               error = "child \"" + childSpec + "\".nextRun()"
+                                                   + " returned null - should be exactly one run";
+                                           } else {
+                                               childIterator = null;
+                                               return result;
+                                           }
+                                       }
+                                   } else {
+                                       childIterator = iter;
+                                   }
+                               }
+                           }
+                       }
+                   }
+                   if (null != childIterator) {
+                       result = runner.wrap(childIterator, null);
+                       childIterator = null;
+                   } else if (null != error) {
+                       numIncomplete++;
+                       if (null != failureKind) {
+                           handler.handleMessage(new Message(specLabel + error, failureKind, null, null));
+                       }
+                       if (!haltOnFailure) {
+                           error = null;
+                       } else if (result != null) {
+                           result = null; // do not return result if halting due to failure
+                       }
+                   }
+               }
+               return result;
+               } finally {
+                       validator.popHandler(handler);
+               }
+    }
+
+       /**
+        * @see org.aspectj.testing.run.IRunIterator#iterationCompleted()
+        */
+       public void iterationCompleted() {
+       }
+   
+    public String toString() {
+        return "" + spec;
+        //return "RunSpecIterator(" + specIndex + ", " + spec + ")" ;
+    }
+    
+    /*
+     * Subclasses may:
+     * - set the sandbox on construction
+     * - lazily set it on first use
+     * - set it for each child
+     */    
+
+    /**
+     * Create the sandbox used for each child.
+     * This implementation always uses the sandbox set on construction.
+     * Subclasses may decide to create one sandbox per child iterator.
+     */
+    protected Sandbox makeSandbox(IRunSpec child, Validator validator) {
+        return getSandbox();
+    }
+
+    /** Get the sandbox currently in use */
+    protected Sandbox getSandbox() {
+        return sandbox;
+    }
+
+    /** Set the sandbox currently in use */
+    protected void setSandbox(Sandbox sandbox) {
+        LangUtil.throwIaxIfNull(sandbox, "sandbox");
+        this.sandbox = sandbox;
+    }
+    
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/Sandbox.java b/testing/src/org/aspectj/testing/harness/bridge/Sandbox.java
new file mode 100644 (file)
index 0000000..6f4984b
--- /dev/null
@@ -0,0 +1,386 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.ICommand;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.testing.util.Diffs;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * A sandbox holds state shared by AjcTest sub-runs,
+ * mostly directories relevant to testing.
+ * It permits a limited amount of coordination and
+ * setup/cleanup operations (todo XXX).
+ * <p>
+ * AjcTest creates the Sandbox and initializes the final fields.
+ * To coordinate with each other, run components may set and get values, 
+ * with the sources running first and the sinks second.  
+ * To make the interactions clear 
+ * (and to avoid accidentally violating these semantics),
+ * setters/getters for a coordinated property are constrained two ways:
+ * <li>Both have an extra (typed) "caller" parameter which must not
+ *     be null, authenticating that the caller is known & valid.</li>
+ * <li>A getter throws IllegalStateException if called before the setter</li>
+ * <li>A setter throws IllegalStateException if called after the getter<li>
+ * XXX subclass more general sandbox?
+ */
+public class Sandbox {
+    /** classes directory token for DirChanges.Spec */
+    public static final String RUN_DIR = "run";
+    
+    /** run directory token for DirChanges.Spec */
+    public static final String CLASSES_DIR = "classes";
+    
+    private static boolean canRead(File dir) {
+        return ((null != dir) && dir.isDirectory() && dir.canRead());
+    }
+
+    private static boolean canWrite(File dir) {
+        return ((null != dir) && dir.isDirectory() && dir.canWrite());
+    }
+
+    private static void iaxWrite(File dir, String label) {
+        if (!canWrite(dir)) {
+            throw new IllegalArgumentException(label + " - " + dir);
+        }
+    }
+
+    private static void iaxRead(File dir, String label) {
+        if (!canRead(dir)) {
+            throw new IllegalArgumentException(label + " - " + dir);
+        }
+    }
+    
+    /** @throws IllegalStateException(message) if test */
+    private static void assertState(boolean test, String message) {
+        if (!test) {
+            throw new IllegalStateException(message);
+        }
+    }
+    
+    /** 
+     * The (read-only) base of the test sources (which may or may not
+     * be the base of the java sources)
+     */
+    public final File testBaseDir;
+    
+    /** the parent of a temporary workspace, probably includes some others */
+    public final File sandboxDir;
+
+    /** a shared working dir */
+    public final File workingDir;
+
+    /** a shared classes dir */
+    public final File classesDir;
+
+    /** a run dir (which will be ignored in non-forking runs) */
+    public final File runDir;
+    
+    /** staging directory for IAjcRun requiring files be copied, deleted, etc. */
+    public final File stagingDir;
+    
+    /** 
+     * This manages creation and deletion of temporary directories.
+     * We hold a reference so that our clients can signal whether 
+     * this should be deleted.
+     */
+    private final Validator validator; // XXX required after completing tests? 
+    
+    /** original base of the original java sources, set by CompileRun.setup(..) */
+    private File testBaseSrcDir;
+
+    /** directories and libraries on the classpath, set by CompileRun.setup(..)  */
+    private File[] classpath;
+
+    /** track whether classpath getter ran */
+    private boolean gotClasspath;
+        
+    /** command shared between runs using sandbox - i.e., compiler */
+    private ICommand command;
+
+    /** track whether command getter ran */
+    private boolean gotCommand;
+        
+    /** cache results of rendering final fields */
+    private transient String toStringLeader;
+    
+    /** @throws IllegalArgumentException unless validator validates
+     *           testBaseDir as readable
+     */
+    public Sandbox(File testBaseDir, Validator validator) {
+        LangUtil.throwIaxIfNull(validator, "validator");
+        this.validator = validator;
+        Sandbox.iaxRead(testBaseDir, "testBaseDir");
+        this.testBaseDir = testBaseDir;
+        
+        sandboxDir = FileUtil.getTempDir("Sandbox");
+        Sandbox.iaxWrite(sandboxDir, "sandboxDir"); // XXX not really iax
+
+        workingDir = FileUtil.makeNewChildDir(sandboxDir, "workingDir");
+        Sandbox.iaxWrite(workingDir, "workingDir");             
+
+        classesDir = FileUtil.makeNewChildDir(sandboxDir, "classes");
+        Sandbox.iaxWrite(classesDir, "classesDir");             
+
+        runDir = FileUtil.makeNewChildDir(sandboxDir, "run");
+        Sandbox.iaxWrite(runDir, "runDir"); 
+
+        stagingDir = FileUtil.makeNewChildDir(sandboxDir, "staging");
+        Sandbox.iaxWrite(stagingDir, "stagingDir"); 
+
+        validator.registerSandbox(this);        
+    }
+
+    private String getToStringLeader() {
+        if (null == toStringLeader) {
+            toStringLeader = "Sandbox(" + sandboxDir.getName() 
+                + ", " + testBaseSrcDir.getName(); 
+        }
+        return toStringLeader;
+    }
+    
+    /** @return "Sandbox(sandbox, src, classes)" with names only */
+    public String toString() {
+        return getToStringLeader() + ", " + classesDir.getName() + ")";
+    }
+    
+    /** @return "Sandbox(sandbox, src, classes)" with paths */
+    public String toLongString() {
+        return getToStringLeader() + ", " + classesDir.getPath()
+            + (null == command ? ", (null command)" : ", " + command) + ")";
+    }
+    
+    void setCommand(ICommand command, CompilerRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller"); 
+        LangUtil.throwIaxIfNull(command, "command"); 
+        LangUtil.throwIaxIfFalse(!gotCommand, "no command"); 
+        this.command = command;
+    }
+    
+    /** When test is completed, clear the compiler to avoid memory leaks */
+    void clearCommand(AjcTest caller) {
+        LangUtil.throwIaxIfNull(caller, "caller"); 
+        if (null != command) {
+            command = null;
+        }
+    }
+    
+//    /** 
+//     * Populate the staging directory by copying any files in the
+//     * source directory ending with fromSuffix 
+//     * to the staging directory, after renaming them with toSuffix.
+//     * If the source file name starts with "delete", then the
+//     * corresponding file in the staging directory is deleting.
+//     * @return a String[] of the files copied or deleted
+//     *          (path after suffix changes and relative to staging dir)
+//     * @throws Error if no File using fromSuffix are found
+//     */
+//    String[] populateStagingDir(String fromSuffix, String toSuffix, IAjcRun caller) {
+//        LangUtil.throwIaxIfNull(fromSuffix, "fromSuffix");
+//        LangUtil.throwIaxIfNull(toSuffix, "toSuffix");
+//        LangUtil.throwIaxIfNull(caller, "caller");
+//
+//        ArrayList result = new ArrayList();
+//        FileUtil.copyDir(
+//            srcBase,
+//            targetSrc,
+//            fromSuffix,
+//            toSuffix,
+//            collector);
+//
+//        final String canonicalFrom = srcBase.getCanonicalPath();
+//        final Definition[] defs = getDefinitions(srcBase);
+//        if ((null == defs) || (defs.length < 9)) {
+//            throw new Error("did not get definitions");
+//        }
+//        MessageHandler compilerMessages = new MessageHandler();
+//        StringBuffer commandLine = new StringBuffer();
+//        for (int i = 1; result && (i < 10); i++) { 
+//            String fromSuffix = "." + i + "0.java";
+//            // copy files, collecting as we go...
+//            files.clear();
+//            if (0 == files.size()) { // XXX detect incomplete?
+//                break;
+//            }
+//
+//        
+//        return (String[]) result.toArray(new String[0]);        
+//    }
+        
+    // XXX move to more general in FileUtil
+    void reportClassDiffs(
+        final IMessageHandler handler, 
+        IncCompilerRun caller,
+        long classesDirStartTime,
+        String[] expectedSources) {
+        LangUtil.throwIaxIfFalse(0 < classesDirStartTime, "0 >= " + classesDirStartTime);
+        boolean acceptPrefixes = true;
+        Diffs diffs = org.aspectj.testing.util.FileUtil.dirDiffs(
+            "classes", 
+            classesDir, 
+            classesDirStartTime, 
+            ".class", 
+            expectedSources, 
+            acceptPrefixes);
+        diffs.report(handler, IMessage.ERROR);
+    }
+
+//    // XXX replace with IMessage-based implementation
+//    // XXX move to more general in FileUtil
+//    void reportClassesDirDiffs(final IMessageHandler handler, IncCompilerRun caller,
+//                               String[] expectedSources) {
+//        // normalize sources to ignore
+//        final ArrayList sources = new ArrayList();
+//        if (!LangUtil.isEmpty(expectedSources)) {
+//            for (int i = 0; i < expectedSources.length; i++) {
+//                String srcPath = expectedSources[i];
+//                int clip = FileUtil.sourceSuffixLength(srcPath);
+//                if (0 != clip) {
+//                    srcPath = srcPath.substring(0, srcPath.length() - clip);
+//                    sources.add(FileUtil.weakNormalize(srcPath));
+//                } else if (srcPath.endsWith(".class")) {
+//                    srcPath = srcPath.substring(0, srcPath.length() - 6);
+//                    sources.add(FileUtil.weakNormalize(srcPath));
+//                } else {
+//                    MessageUtil.info(handler, "not source file: " + srcPath);
+//                }
+//                     }
+//        }
+//        
+//        // gather, normalize paths changed
+//        final ArrayList changed = new ArrayList();
+//        FileFilter touchedCollector = new FileFilter() {
+//                     public boolean accept(File file) {
+//                if (file.lastModified() > classesDirTime) {
+//                    String path = file.getPath();
+//                    if (!path.endsWith(".class")) {
+//                        MessageUtil.info(handler, "changed file not a class: " + file);
+//                    } else {
+//                        String classPath = path.substring(0, path.length() - 6);
+//                        classPath = FileUtil.weakNormalize(classPath);
+//                        if (sources.contains(classPath)) {
+//                            sources.remove(classPath);
+//                        } else {
+//                            changed.add(classPath);
+//                        }                      
+//                    }
+//                }
+//                return false;
+//                     }
+//        };      
+//        classesDir.listFiles(touchedCollector);
+//        
+//        // report any unexpected changes
+//        Diffs diffs = new Diffs("classes", sources, changed, String.CASE_INSENSITIVE_ORDER);
+//        diffs.report(handler, IMessage.ERROR);
+//    }
+    
+    ICommand getCommand(IncCompilerRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        assertState(null != command, "command never set"); 
+        return command;
+    }
+
+    File getTestBaseSrcDir(IncCompilerRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        return testBaseSrcDir;
+    }
+    
+    File getTestBaseSrcDir(JavaRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        return testBaseSrcDir;
+    }
+    
+    /** @throws IllegalArgumentException unless a readable directory */
+    void setTestBaseSrcDir(File dir, CompilerRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        if ((null == dir) || !dir.isDirectory() || !dir.canRead()) {
+            throw new IllegalArgumentException("bad test base src dir: " + dir);
+        }
+        testBaseSrcDir = dir;
+    }
+    
+    /** @param readable if true, then throw IllegalArgumentException if not readable */
+    void setClasspath(File[] files, boolean readable, CompilerRun caller) {
+        LangUtil.throwIaxIfNull(files, "files");
+        LangUtil.throwIaxIfNull(caller, "caller");
+        assertState(!gotClasspath, "classpath already read");
+        classpath = new File[files.length];
+        for (int i = 0; i < files.length; i++) {
+            LangUtil.throwIaxIfNull(files[i], "files[i]");
+            if (readable && !files[i].canRead()) {
+                throw new IllegalArgumentException("bad classpath entry: " + files[i]);
+            }
+            classpath[i] = files[i];
+               }
+    }
+
+    File[] getClasspath(JavaRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        assertState(null != classpath, "classpath not set");
+        
+        File[] result = new File[classpath.length];
+        System.arraycopy(classpath, 0, result, 0, result.length);
+        return result;
+    }
+    
+    /** @param readable if true, omit non-readable directories */
+    File[] getClasspathDirectories(boolean readable, JavaRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        assertState(null != classpath, "classpath not set");
+        ArrayList result = new ArrayList();
+        File[] src = classpath;
+        for (int i = 0; i < src.length; i++) {
+                       File f = src[i];
+            if ((null != f) && (f.isDirectory()) && (!readable || f.canRead())) {
+                result.add(f);
+            }
+               }
+        return (File[]) result.toArray(new File[0]);
+    }
+
+    /** @param readable if true, omit non-readable directories */
+    File[] getClasspathJars(boolean readable, JavaRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        assertState(null != classpath, "classpath not set");
+        ArrayList result = new ArrayList();
+        File[] src = classpath;
+        for (int i = 0; i < src.length; i++) {
+            File f = src[i];
+            if (FileUtil.hasZipSuffix(f) && (!readable || f.canRead())) {
+                result.add(f);
+            }
+        }
+        return (File[]) result.toArray(new File[0]);
+    }
+    
+    /** @return String of classpath entries delimited internally by File.pathSeparator */
+    String classpathToString(CompilerRun caller) {
+        LangUtil.throwIaxIfNull(caller, "caller");
+        assertState(null != classpath, "classpath not set");
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < classpath.length; i++) {
+          if (i > 0) {
+            sb.append(File.pathSeparator);
+          }
+          sb.append(classpath[i].getAbsolutePath());    
+        }
+        return sb.toString();
+    }
+}
diff --git a/testing/src/org/aspectj/testing/harness/bridge/Validator.java b/testing/src/org/aspectj/testing/harness/bridge/Validator.java
new file mode 100644 (file)
index 0000000..55c3962
--- /dev/null
@@ -0,0 +1,557 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.ListIterator;
+import java.util.Stack;
+
+/** 
+ * Check input and implement defaults.
+ * This handles failure messaging and collecting temp directories
+ * for later cleanup.
+ * The default behavior is to send a fail message to the message
+ * handler.  Clients may instead:
+ * <li>Toggle abortOnException to throw AbortException on failure</li>
+ * <li>push their own message handler to redirect messages
+ *     (pop to remove if any pushed)</li>
+ * <li>subclass this to reimplement <code>fail(String)</code>
+ * <p>
+ * A component given this to do validation should
+ * not change the reporting scheme established by the caller,
+ * so the caller may lock and unlock the error handling policy.
+ * When the policy is locked, this silently ignores attempts
+ * to toggle exceptions, or delete temporary files. 
+ * XXX callers cannot prevent others from pushing other error handlers.
+ */
+public class Validator {
+    
+    /** stack of handlers */
+    private final Stack handlers;
+    
+    /** list of File registered for deletion on demand */
+    private final ArrayList tempFiles; // deleteTempFiles requires ListIterator.remove()
+    
+    /** list of Sandboxes registered for cleanup on demand  */
+    private final ArrayList sandboxes;
+    
+    /** if true, throw AbortException on failure */
+    boolean abortOnFailure;
+    
+    /** this object prevents any changes to error-handling policy */
+    private Object locker;
+    
+    public Validator(IMessageHandler handler) {
+        tempFiles = new ArrayList();
+        sandboxes = new ArrayList();
+        handlers = new Stack();
+        pushHandler(handler);
+    } 
+    
+    /** 
+     * Push IMessageHandler onto stack,
+     * so it will be used until the next push or pop
+     * @param handler not null
+     * 
+     */
+    public void pushHandler(IMessageHandler handler) {
+       LangUtil.throwIaxIfNull(handler, "handler");
+               handlers.push(handler);
+    }
+    
+    /** @throws IllegalStateException if handler is not on top */
+    public void popHandler(IMessageHandler handler) {
+       LangUtil.throwIaxIfNull(handler, "handler");
+               if (handler != handlers.peek()) {
+                       throw new IllegalStateException("not current handler");
+               }
+               handlers.pop();
+    }
+    
+    /** @return true if this requestor now has locked the error handling policy */
+    public boolean lock(Object requestor) {
+        if (null == locker) {
+            locker = requestor;
+        }
+        return (locker == requestor);
+    }
+
+    /** @return true if the error handling policy is now unlocked  */
+    public boolean unlock(Object requestor) {
+        if (requestor == locker) {
+            locker = null;
+        }
+        return (locker == null);
+    }
+    
+    public void setAbortOnFailure(boolean abortOnFailure) {
+        if (null == locker) {
+            if (this.abortOnFailure != abortOnFailure) {
+                this.abortOnFailure = abortOnFailure;
+            }
+        }
+    }
+        
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} array<li>
+     * <li>{null check} {message}[#}<li>
+     */
+    public boolean nullcheck(Object[] ra, String message) {
+        return ((nullcheck((Object) ra, message + " array"))
+                && nullcheck(Arrays.asList(ra), message));
+    }
+    
+    /**
+     * Like nullcheck(Collection, message), except adding lower and upper bound
+     * @param atLeast fail if list size is smaller than this
+     * @param atMost  fail if list size is greater than this
+     */
+    public boolean nullcheck(Collection list, int atLeast, int atMost, String message) {
+        if (nullcheck(list, message)) {
+            int size = list.size();
+            if (size < atLeast) {
+                fail(message + ": " + size + "<" + atLeast);
+            } else if (size > atMost) {
+                fail(message + ": " + size + ">" + atMost);
+            } else {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} list<li>
+     * <li>{null check} {message}[#}<li>
+     */
+    public boolean nullcheck(Collection list, String message) {
+        if (nullcheck((Object) list, message + " list")) {
+            int i = 0;
+            for (Iterator iter = list.iterator(); iter.hasNext();) {
+                               if (!nullcheck(iter.next(), message + "[" + i++ + "]")) {
+                            return false;
+                }              
+                       }
+            return true;
+        }
+        return false;
+    }
+    
+    /** 
+     * May fail with the message "null {message}" 
+     * if o and the def{ault} are null
+     * @return o if not null or default otherwise
+     */
+    public Object nulldefault(Object o, String message, Object def) {
+        if (null == o) {
+            o = def;
+        }
+        nullcheck(o, message);
+        return o;
+    }
+
+    /** may fail with the message "null {message}" */
+    public boolean nullcheck(Object o, String message) {
+        if (null == o) {
+            if (null == message) message = "object";
+            fail("null " + message);
+            return false;
+        }
+        return true;
+    }
+    
+    /**
+     * Verify that all paths are readable relative to baseDir. 
+     * may fail with the message "cannot read {file}" 
+     */
+    public boolean canRead(File baseDir, String[] paths, String message) {
+        if (!canRead(baseDir, "baseDir - " + message)
+            || !nullcheck(paths, "paths - " + message)) {
+            return false;
+        }
+        final String dirPath = baseDir.getPath();
+        File[] files = FileUtil.getBaseDirFiles(baseDir, paths);
+        for (int j = 0; j < files.length; j++) {
+            if (!canRead(files[j], "{" + dirPath + "} " + files[j])) {
+                return false;
+            }           
+        }
+        return true;
+    }
+    
+    /**
+     * Verify that all paths are readable relative to baseDir. 
+     * may fail with the message "cannot read {file}" 
+     */
+    public boolean canRead(File[] files, String message) {
+        if (!nullcheck(files, message)) {
+            return false;
+        }
+        for (int j = 0; j < files.length; j++) {
+            if (!canRead(files[j], files[j].getPath())) {
+                return false;
+            }           
+        }
+        return true;
+    }
+
+    /** may fail with the message "cannot read {file}" */
+    public boolean canRead(File file, String message) {
+        if (nullcheck(file, message)) {
+            if (file.canRead()) {
+                return true;
+            } else {
+                fail("cannot read " + file);
+            }
+        }
+        return false;
+    }
+    
+    /** may fail with the message "cannot write {file}" */
+    public boolean canWrite(File file, String message) {
+        if (nullcheck(file, message)) {
+            if (file.canRead()) {
+                return true;
+            } else {
+                fail("cannot write " + file);
+            }
+        }
+        return false;
+    }
+    
+    /** may fail with the message "not a directory {file}" */
+    public boolean canReadDir(File file, String message) {
+        if (canRead(file, message)) {
+            if (file.isDirectory()) {
+                return true;
+            } else {
+                fail("not a directory " + file);
+            }
+        }
+        return false;
+    }
+    
+    /** may fail with the message "not a directory {file}" */
+    public boolean canWriteDir(File file, String message) {
+        if (canWrite(file, message)) {
+            if (file.isDirectory()) {
+                return true;
+            } else {
+                fail("not a directory " + file);
+            }
+        }
+        return false;
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} dir array<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canRead} {message}[#}<li>
+     */
+    public boolean canReadFiles(Object[] dirs, String message) {
+        return ((nullcheck((Object) dirs, message + " dir array"))
+                && canReadFiles(Arrays.asList(dirs), message));
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} files<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canRead} {message}[#}<li>
+     */
+    public boolean canReadFiles(Collection dirs, String message) {
+        if (nullcheck((Object) dirs, message + " files")) {
+            int i = 0;
+            for (Iterator iter = dirs.iterator(); iter.hasNext();) {
+                Object o = iter.next();
+                if (! (o instanceof File)) {
+                    fail(i + ": not a file " + o);
+                }                
+                if (!canRead((File) o, message + "[" + i++ + "]")) {
+                     return false;
+                }       
+            }
+            return true;
+        }
+        return false;
+    }
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} dir array<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canReadDir} {message}[#}<li>
+     */
+    public boolean canReadDirs(Object[] dirs, String message) {
+        return ((nullcheck((Object) dirs, message + " dir array"))
+                && canReadDirs(Arrays.asList(dirs), message));
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} dirs<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canReadDir} {message}[#}<li>
+     */
+    public boolean canReadDirs(Collection dirs, String message) {
+        if (nullcheck((Object) dirs, message + " dirs")) {
+            int i = 0;
+            for (Iterator iter = dirs.iterator(); iter.hasNext();) {
+                Object o = iter.next();
+                if (! (o instanceof File)) {
+                    fail(i + ": not a file " + o);
+                }                
+                if (!canReadDir((File) o, message + "[" + i++ + "]")) {
+                     return false;
+                }       
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} dir array<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canWrite} {message}[#}<li>
+     */
+    public boolean canWriteFiles(Object[] dirs, String message) {
+        return ((nullcheck((Object) dirs, message + " dir array"))
+                && canWriteFiles(Arrays.asList(dirs), message));
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} files<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canWrite} {message}[#}<li>
+     */
+    public boolean canWriteFiles(Collection dirs, String message) {
+        if (nullcheck((Object) dirs, message + " files")) {
+            int i = 0;
+            for (Iterator iter = dirs.iterator(); iter.hasNext();) {
+                Object o = iter.next();
+                if (! (o instanceof File)) {
+                    fail(i + ": not a file " + o);
+                }                
+                if (!canWrite((File) o, message + "[" + i++ + "]")) {
+                     return false;
+                }       
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} dir array<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canWriteDir} {message}[#}<li>
+     */
+    public boolean canWriteDirs(Object[] dirs, String message) {
+        return ((nullcheck((Object) dirs, message + " dir array"))
+                && canWriteDirs(Arrays.asList(dirs), message));
+    }
+    
+    /** 
+     * May fail with any of the messages
+     * <li>{null check} dirs<li>
+     * <li>"#: not a File {file}"<li>
+     * <li>{canWriteDir} {message}[#}<li>
+     */
+    public boolean canWriteDirs(Collection dirs, String message) {
+        if (nullcheck((Object) dirs, message + " dirs")) {
+            int i = 0;
+            for (Iterator iter = dirs.iterator(); iter.hasNext();) {
+                Object o = iter.next();
+                if (! (o instanceof File)) {
+                    fail(i + ": not a file " + o);
+                }                
+                if (!canWriteDir((File) o, message + "[" + i++ + "]")) {
+                     return false;
+                }       
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /** 
+     * Send an info message to any underlying handler 
+     * @param message ignored if null
+     */
+    public void info(String message) {
+        if (null != message) {
+               IMessageHandler handler = getHandler();
+            MessageUtil.info(handler, message);
+        }
+    }
+    
+    /** Fail via message or AbortException */
+    public void fail(String message) {
+        fail(message, (Throwable) null);
+    }
+    
+    /** 
+     * Fail via message or AbortException.
+    * All failure messages go through here,
+    * so subclasses may override to control
+    * failure-handling.
+    */
+    public void fail(String message, Throwable thrown) {
+        if ((null == message) && (null == thrown)) {
+            message = "<Validator:no message>";
+        }
+        IMessage m = MessageUtil.fail(message, thrown);
+        if (abortOnFailure) {
+            throw new AbortException(m);
+        } else {
+               IMessageHandler handler = getHandler();
+            handler.handleMessage(m);
+        }
+    }
+    
+    /** 
+     * Register a file temporary, i.e., to be
+     * deleted on completion.  The file need not
+     * exist (yet or ever) and may be a duplicate
+     * of existing files registered.
+     */
+    public void registerTempFile(File file) {
+        if (null != file) {
+            tempFiles.add(file);
+        }
+    }
+    
+    /** 
+     * Get a writable {possibly-empty} directory.
+     * If the input dir is null, then try to create a temporary
+     * directory using name.  
+     * If the input dir is not null, this tries to 
+     * create it if it does not exist.  
+     * Then if name is not null,
+     * it tries to get a temporary directory
+     * under this using name.
+     * If name is null, "Validator" is used.
+     * If deleteContents is true, this will try to delete
+     * any existing contents.  If this is unable to delete
+     * the contents of the input directory, this may return
+     * a new, empty temporary directory.
+     * If register is true, then any directory returned is
+     * saved for later deletion using <code>deleteTempDirs()</code>,
+     * including the input directory.
+     * When this is unable to create a result, if failMessage
+     * is not null then this will fail; otherwise it returns false;
+     */
+    public File getWritableDir(File dir, String name, 
+        boolean deleteContents, boolean register, String failMessage) {
+        // check dir
+        if (null == dir) {
+            if (null == name) {
+                name = "Validator";
+            }
+            dir = FileUtil.getTempDir(name);            
+        } else { 
+            if (!dir.exists()) { 
+                dir.mkdirs();
+            }
+        }
+        // fail if necessary
+        if ((null == dir) || (!dir.exists())) {
+            if (null != failMessage) {
+                fail(failMessage + ": unable to get parent " + dir);
+            }
+        } else {
+            File result = FileUtil.makeNewChildDir(dir, name);
+            if (deleteContents) {
+                FileUtil.deleteContents(dir);
+            }
+            if (register) {
+                tempFiles.add(dir);
+            }
+        }
+        return dir;
+    }
+
+    /** 
+     * Delete any temp sandboxes, files or directories saved,
+     * optionally reporting failures in the normal way.
+     * This will be ignored unless the failure policy
+     * is unlocked.
+     */
+    public void deleteTempFiles(boolean reportFailures) {
+        if (null == locker) {
+            for (ListIterator iter = tempFiles.listIterator(); iter.hasNext();) {
+                deleteFile((File) iter.next(), reportFailures);
+            }
+            for (ListIterator iter = sandboxes.listIterator(); iter.hasNext();) {
+                Sandbox sandbox = (Sandbox) iter.next();                
+                // XXX assumes all dirs are in sandboxDir
+                deleteFile(sandbox.sandboxDir, reportFailures);
+            }
+        }
+    }                
+
+       /**
+        * Manage temp files and directories of registered sandboxes.
+     * Note that a sandbox may register before it is initialized,
+     * so this must do nothing other than save the reference.
+        * @param sandbox the uninitialized Sandbox to track
+        */
+       public void registerSandbox(Sandbox sandbox) {
+          sandboxes.add(sandbox);
+    }
+    
+    
+    private void deleteFile(File file, boolean reportFailures) {
+        if (null == file) {
+            if (reportFailures) {
+                fail("unable to delete null file");
+            }
+            return;
+        }
+        FileUtil.deleteContents(file);
+        if (file.exists()) {
+            file.delete();
+        }
+        if (reportFailures && file.exists()) {
+            fail("unable to delete " + file);
+        }
+    }
+    
+    /** @throws IllegalStateException if handler is null */
+       private IMessageHandler getHandler() {
+               IMessageHandler handler = (IMessageHandler) handlers.peek();
+               if (null == handler) {
+                       throw new IllegalStateException("no handler");
+               }
+               return handler;
+       }
+
+} // class Validator
diff --git a/testing/src/org/aspectj/testing/run/IRun.java b/testing/src/org/aspectj/testing/run/IRun.java
new file mode 100644 (file)
index 0000000..8cc33f6
--- /dev/null
@@ -0,0 +1,62 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+/**
+ * A run is a Runnable that may know how to set its own status.
+ * @author isberg
+ */
+public interface IRun {
+    public static final IRun[] RA_IRun = new IRun[0];
+
+    /** Positive wrapper for the status parameter */
+    public static final IRun OK 
+        = new IRun() {
+                /** This returns false when the status is null 
+                 * or runResult is false */
+                public boolean run(IRunStatus status) {
+                    return ((null != status) && status.runResult());
+                }
+                public IRunStatus makeStatus() { return null; }
+            };
+
+    /** Negative wrapper for the status parameter */
+    public static final IRun NOTOK 
+        = new IRun() {
+                public boolean run(IRunStatus status) {
+                    return ((null == status) || !status.runResult());
+                }
+                public IRunStatus makeStatus() { return null; }
+            };
+
+    /**
+     * Run process, setting any known status.
+     * Normally the caller starts the process
+     * and the callee completes it, so that
+     * status.isCompleted() returns true after
+     * the call completes.  However, responsible
+     * callees ensure starting, and responsible
+     * callers ensure completed after the call.
+     * Anyone setting completion should ensure it
+     * is set recursively for all children, 
+     * and anyone starting child runs should
+     * ensure children are registered and initialized
+     * appropriately.
+     * @param status the IRunStatus representing the 
+     *  outcome of the process (collecting parameter).
+     * @see Runners
+     */
+    boolean run(IRunStatus status) throws Exception; // IMessageHandler?
+}
diff --git a/testing/src/org/aspectj/testing/run/IRunIterator.java b/testing/src/org/aspectj/testing/run/IRunIterator.java
new file mode 100644 (file)
index 0000000..34bc12d
--- /dev/null
@@ -0,0 +1,62 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessageHandler;
+
+/**
+ * Iterator for IRun.
+ * IRunIterator are useful if the underlying components
+ * can use a generic IRunStatus and a single listener.
+ * It is a requirement of any component runnin the IRunIterator
+ * that they call iterationCompleted() when done, to permit
+ * the IRunIterator to clean up.
+ * @see Runner#runIterator(IRunIterator, IRunStatus, IRunListener)
+ */
+public interface IRunIterator {
+
+    /** 
+     * @return true if nextRun() would return something non-null 
+     * @throws IllegalStateException if called after 
+     *          <code>iterationCompleted()</code>
+     */
+    boolean hasNextRun();
+    
+    /**
+     * Get the next run.
+     * IRunIterator which contains child IRunIterator may either return
+     * the children IRun or wrap them using
+     * Runner.wrap(IRunIterator, IRunListener)
+     * @param handler the IMessageHandler to use for error and other messages
+     * @param runnere the Runner to use to wrap any nested IRunIterator as IRun. 
+     * @return the next run, or null if there are no more.
+     * @throws IllegalStateException if called after 
+     *          <code>iterationCompleted()</code>
+     * @see Runner#wrap(IRunIterator, IRunListener)
+     */
+    IRun nextRun(IMessageHandler handler, Runner runner);
+    
+    /**
+     * Signal a runner that further runs should be aborted.  Runners
+     * should check this after each failure.
+     * @return true if the runner should stop iterating when an IRun fails
+     * @throws IllegalStateException if called after 
+     *          <code>iterationCompleted()</code>
+     */
+    boolean abortOnFailure(); // XXX supply IRun or IRunStatus?
+    
+    /** called when hasNextRun() and nextRun() will no longer be called */
+    void iterationCompleted();
+}
diff --git a/testing/src/org/aspectj/testing/run/IRunListener.java b/testing/src/org/aspectj/testing/run/IRunListener.java
new file mode 100644 (file)
index 0000000..85ab614
--- /dev/null
@@ -0,0 +1,38 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+/**
+ * Listen to events in the run lifecycle -
+ * birth, death, and procreation.
+ * @author isberg
+ */
+public interface IRunListener {
+
+    /**
+     * Called when run is about to be started.
+     */
+    void runStarting(IRunStatus run);
+
+    /**
+     * Called when run is has completed.
+     */
+    void runCompleted(IRunStatus run);
+
+    /**
+     * Called when adding a child to a parent run
+     */
+    void addingChild(IRunStatus parent, IRunStatus child);
+}
diff --git a/testing/src/org/aspectj/testing/run/IRunStatus.java b/testing/src/org/aspectj/testing/run/IRunStatus.java
new file mode 100644 (file)
index 0000000..6651c42
--- /dev/null
@@ -0,0 +1,175 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageUtil;
+
+/**
+ * Encapsulate status and results for a run.
+ * A run starts and then completes normally
+ * (finished(Object result)), 
+ * abruptly (thrown(Throwable thrown)),
+ * or by user request (abort(Object request)).
+ * @author isberg
+ */
+public interface IRunStatus extends IMessageHolder {
+    /** clients use this when signalling completion without a specific result */
+    public static final Object VOID = Boolean.TRUE;
+
+    /** result object for successful (unset) boolean run result */
+    public static final Boolean PASS = Boolean.TRUE;
+        
+    /** result object for failed (unset) boolean run result */
+    public static final Boolean FAIL = Boolean.FALSE;
+    
+    /** clients use this when signalling abort without any specific request */
+    public static final Object ABORT = Boolean.FALSE;
+    
+    /** clients use this when signalling abort because no object to run */
+    public static final Object ABORT_NORUN = MessageUtil.ABORT_NOTHING_TO_RUN;
+    
+    /** returned from getChildren when there are no children */
+    public static final IRunStatus[] EMPTY_NEST = new IRunStatus[0];
+    
+    //------------------- process controls
+    /**
+     * Set identifier associated with this run, if any
+     * @throws IllegalArgumentException if id is null
+     * @throws IllegalStateException if id has already been set
+     */
+    void setIdentifier(Object id);
+
+    //------------------- process controls
+    /**
+     * Call before any start() or after isCompleted() would return true
+     * to reset this to its pre-start state
+     * @throws IllegalStateException if start() has been called 
+     * and isCompleted() is not true.
+     */
+    void reset();
+    
+    /** 
+     * Call only once to signal this run has started.
+     * @throws IllegalStateException if start() has been called
+     */
+    void start();
+    
+    /** 
+     * Call this or thrown only once after start() 
+     * to signal this run has ended.
+     * If this represents a void process, use VOID.
+     * @param result the Object returned by this run.
+     * @throws IllegalStateException if start() was not called first
+     *  or if either completed(Object) or thrown(Throwable) have been called.
+     */
+    void finish(Object result);
+    
+    /** 
+     * Call to signal this run is ending by request.
+     * If there is no message, use ABORT.
+     * @param request the Object request to abort,
+     *         or ABORT if none is available.
+     * @throws IllegalStateException if start() was not called first
+     *  or if either completed(Object) or thrown(Throwable) have been called.
+     */
+    void abort(Object request);
+    
+    /** 
+     * Call this or completed only once after start() 
+     * to signal this run has ended.
+     * @throws IllegalStateException if start() was not called first
+     *  or if either completed(Object) or thrown(Throwable) have been called.
+     */
+    void thrown(Throwable thrown);
+    
+    
+    /**
+     * Call this for the status to throw an unchecked exception
+     * of the type that its controller understands.
+     * It is an error for a IRunStatus to continue normally 
+     * after this is invoked.
+     */
+    void completeAbruptly();
+    //------------------- process messages
+    /** 
+     * Call this any time to signal any messages.
+     * (In particular, the IRun caller may use this to register messages
+     * about the mishandling of the run by the ResultStatusI by the callee.)
+     * This is a shortcut for getMessageHandler().handleMessage(..);
+     */
+    //boolean handleMessage(IMessage message);
+    
+    //------------------- process display
+    /** @return true if this run has started */
+    boolean started();
+    
+    /** @return true if one of the result, abort request, or thrown is available */
+    boolean isCompleted();
+
+    /** @return true if this got an abort request */
+    boolean aborted();
+    
+    /** 
+     * @return true if completed and not aborted and no thrown 
+     * or messages with kind ABORT or FAIL or ERROR
+     */
+    boolean runResult();
+
+    /** get the invoker for any subruns */
+    Runner getRunner();
+    
+    /** @return the Object result, if any, of this run */
+    Object getResult();
+
+    /** @return the Object abort request, if any, of this run */
+    Object getAbortRequest();
+
+    /** @return the Throwable thrown, if any, by this run */
+    Throwable getThrown();    
+    
+    /** @return any Message[] signalled, or SILENCE if none */
+    IMessage[] getMessages();
+    
+    /** @return the identifier set for this run, if any */
+    Object getIdentifier();
+    
+    //------------------- subprocess
+    /** 
+     * Add a record for a child run
+     * and install self as parent.
+     * @throws IllegalArgumentException if child is null
+     */
+    void addChild(IRunStatus child);
+
+    /**
+     * Register this as the run parent.
+     * (Any run that does addChild(IRunStatus) should register as parent.)
+     * @throws IllegalArgumentException if parent is null
+     * @throws IllegalStateException if parent exists already
+     */
+    void registerParent(IRunStatus parent);
+
+    /** 
+     * @return the current children of this run, or EMPTY_NEST if none
+     */
+    IRunStatus[] getChildren();
+
+    /**
+     * @return the currently-registered parent, or null if none
+     */
+    IRunStatus getParent();
+}
diff --git a/testing/src/org/aspectj/testing/run/IRunValidator.java b/testing/src/org/aspectj/testing/run/IRunValidator.java
new file mode 100644 (file)
index 0000000..29f73f1
--- /dev/null
@@ -0,0 +1,28 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+/**
+ * These check whether particular runs have passed.
+ * @author isberg
+ */
+public interface IRunValidator {
+    /**
+     * Evaluate whether a run has passed.
+     * @param run the IRunStatus to see if it passed. 
+     * @return true if run has passed
+     */
+    boolean runPassed(IRunStatus run);
+}
diff --git a/testing/src/org/aspectj/testing/run/RunIterator.java b/testing/src/org/aspectj/testing/run/RunIterator.java
new file mode 100644 (file)
index 0000000..37e3187
--- /dev/null
@@ -0,0 +1,136 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.LangUtil;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Adapt IRun or Run[] or List or ListIterator to RunIteratorI.
+ */
+public class RunIterator implements IRunIterator {
+    
+    protected String name;
+    protected ListIterator iter;
+    protected IRun run;
+    
+    public RunIterator(String name, IRun run) {
+        init(name, run);
+    }
+
+    public RunIterator(String name, List list) {
+        init(name, list);
+    }
+    
+    public RunIterator(String name, IRun[] runs) {
+        init(name, Arrays.asList(runs).listIterator());
+    }
+
+    public RunIterator(String name, ListIterator iterator) {
+        init(name, iterator);
+    }
+
+    public void init(String name, List list) {
+        init(name, list.listIterator());
+    }
+
+    public void init(String name, IRun[] runs) {
+        init(name, Arrays.asList(runs).listIterator());
+    }
+    
+    /** @return true if the first IRun from nextRun can be the sole IRun */
+    public boolean isHoistable() {
+        return (null != run);
+    }
+    
+    /** 
+     * @param name if null, use iterator.toString();
+     * @param iterator not null
+     * @throws IllegalArgumentException if iterator is null
+     */
+    public void init(String name, ListIterator iterator) {
+        LangUtil.throwIaxIfNull(iterator, "iterator");
+        iter = iterator;
+        name = (null != name? name : iterator.toString());
+        run = null;
+    }
+    
+    /** 
+     * @param name if null, use run();
+     * @param run not null
+     * @throws IllegalArgumentException if iterator is null
+     */
+    public void init(String name, IRun run) {
+        LangUtil.throwIaxIfNull(run, "run");
+        this.run = run;
+        name = (null != name? name : run.toString());
+        iter = null;
+    }
+
+    /**
+     * @return false always
+        * @see org.aspectj.testing.run.IRunIterator#abortOnFailure()
+        */
+       public boolean abortOnFailure() {
+               return false;
+       }
+
+    /**
+        * @see org.aspectj.testing.run.RunIteratorI#hasNextRun()
+        */
+       public boolean hasNextRun() {
+               return ((null != run) || ((null != iter) && (iter.hasNext())));
+       }
+
+    /**
+        * @see org.aspectj.testing.run.IRunIterator#iterationCompleted()
+        */
+       public void iterationCompleted() {
+       }
+
+    
+    /**
+        * @see org.aspectj.testing.run.RunIteratorI#nextRun(IMessageHandler, Runner)
+        */
+       public IRun nextRun(IMessageHandler handler, Runner runner) {
+               if (null != run) {
+            IRun result = run;
+            run = null;
+            return result;
+        }
+        if (null != iter) {
+            for (Object o = iter.next(); iter.hasNext();) {
+                if (o instanceof IRunIterator) {
+                    return runner.wrap((IRunIterator) o, null);
+                } else if (o instanceof IRun) {
+                    return (IRun) o;
+                } else {
+                    MessageUtil.error(handler, "not IRun or IRunIterator: " + o);
+                }
+            }
+        }
+        return null;            
+       }
+
+    /** @return name */
+    public String toString() {
+        return name;
+    }
+}
diff --git a/testing/src/org/aspectj/testing/run/RunListener.java b/testing/src/org/aspectj/testing/run/RunListener.java
new file mode 100644 (file)
index 0000000..5238629
--- /dev/null
@@ -0,0 +1,116 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.MessageUtil;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * A generic RunListener for easier partial implementations.
+ * It can take a RunI selector (called on completion) 
+ * and/or a List to accumulate complete IRunStatus
+ * (if the selector is null or returns true).
+ * It can also take a PrintWriter and String to print traces of each event
+ * as "{prefix} [addingChild|runStarting|runCompleted]({IRunStatus})"
+ */
+public class RunListener implements IRunListener {
+    protected final List list;
+    protected final IRun selector;
+    protected final PrintWriter writer;
+    protected final String prefix;
+     
+    protected RunListener() {
+        this((List) null, (IRun) null, (PrintWriter) null, (String) null);
+    }
+    
+    /**
+     * @param sink the List sink for any IRunStatus if the selector is null
+     *              or returns true for run(IRunStatus) - ignored if null.
+     * @param selector the IRun called on completion,
+     *                  perhaps to select those to be accumulated
+     *                  (should NOT throw Exception)
+     */
+    public RunListener(List sink, IRun selector) {
+        this(sink, selector, (PrintWriter) null, (String) null);
+    }
+
+    /**
+     * @param writer the PrintWriter to print events to - may be null
+     * @param prefix the String prefixing any printing - if null, ""
+     */
+    public RunListener(PrintWriter writer, String prefix) {
+        this((List) null, (IRun) null, writer, prefix);
+    }
+        
+    /**
+     * @param sink the List sink for any IRunStatus if the selector is null
+     *              or returns true for run(IRunStatus) - ignored if null.
+     * @param selector the IRun called on completion,
+     *                  perhaps to select those to be accumulated
+     *                  (should NOT throw Exception)
+     * @param writer the PrintWriter to print events to - may be null
+     * @param prefix the String prefixing any printing - if null, ""
+     */
+    public RunListener(List sink, IRun selector, PrintWriter writer, String prefix) {
+        this.prefix = (null == prefix ? "" : prefix);
+        this.writer = writer;
+        this.selector = selector;
+        list = sink;
+    }
+        
+    /**
+     * @see org.aspectj.testing.harness.run.IRunListener#addingChild(IRunStatus, IRunStatus)
+     */
+    public void addingChild(IRunStatus parent, IRunStatus child) {
+        if (null != writer) {
+            writer.println(prefix + " addingChild(\"" + parent 
+                + "\", \"" + child + "\")");
+        }
+    }
+
+    /**
+        * @see org.aspectj.testing.harness.run.IRunListener#runStarting(IRunStatus)
+        */
+       public void runStarting(IRunStatus run) {
+        if (null != writer) {
+            writer.println(prefix + " runStarting(\"" + run + "\")");
+        }
+       }
+    
+    /**
+     * Print to writer (if any), run selector (if any), and add to list
+     * (if any and if selector is null or returns true).
+        * @see org.aspectj.testing.harness.run.IRunListener#runCompleted(IRunStatus)
+     * @throws AbortException wrapping any Exception thrown by any selector
+     *                         (error for selector to throw Exception)
+        */
+       public void runCompleted(IRunStatus run) {
+        if (null != writer) {
+            writer.println(prefix + " runCompleted(\"" + run + "\")");
+        }
+        try {
+            if (((null == selector) || selector.run(run)) && (null != list)) {
+                list.add(run);
+            }
+        } catch (Throwable e) {
+            String m = "Selectors should not throw exceptions!";
+            throw new AbortException(MessageUtil.abort(m, e));
+        }
+       }
+
+}
diff --git a/testing/src/org/aspectj/testing/run/RunListeners.java b/testing/src/org/aspectj/testing/run/RunListeners.java
new file mode 100644 (file)
index 0000000..9b27d86
--- /dev/null
@@ -0,0 +1,89 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Aggregate listeners into one and run synchronously in order added.
+ * @author isberg
+ */
+public class RunListeners extends RunListener implements IRunListener {
+
+    ArrayList listeners;
+    public RunListeners() {
+        listeners = new ArrayList();
+    }
+    
+    public void addListener(IRunListener listener) {
+        if (null != listener) {
+            listeners.add(listener);
+        }
+    }
+
+    public void removeListener(IRunListener listener) {
+        if (null != listener) {
+            listeners.remove(listener);
+        }
+    }
+    
+
+    /**
+     * Run all listeners with the given status.
+     * @see org.aspectj.testing.harness.run.IRunListener#runStarting(IRunStatus)
+     */
+    public final void runStarting(IRunStatus status) {
+        if (null == status) {
+            throw new IllegalArgumentException("null RunStatusI");
+        }
+        Iterator iter = listeners.iterator(); 
+        while(!status.aborted() && iter.hasNext()) {
+            IRunListener element = (IRunListener) iter.next();
+            element.runStarting(status);
+        }
+    }
+
+    /**
+     * Signal all listeners with the given status.
+     * @see org.aspectj.testing.harness.run.IRunListener#runCompleted(IRunStatus)
+     */
+    public final void runCompleted(IRunStatus status) {
+        if (null == status) {
+            throw new IllegalArgumentException("null RunStatusI");
+        }
+        Iterator iter = listeners.iterator(); 
+        while(!status.aborted() && iter.hasNext()) {
+            IRunListener element = (IRunListener) iter.next();
+            element.runCompleted(status);
+        }
+    }
+       /**
+        * @see org.aspectj.testing.harness.run.IRunListener#addingChild(IRunStatus, IRunStatus)
+        */
+       public final void addingChild(IRunStatus parent, IRunStatus child) {
+        if (null == child) {
+            throw new IllegalArgumentException("null child");
+        }
+        if (null == parent) {
+            throw new IllegalArgumentException("null parent");
+        }
+        Iterator iter = listeners.iterator(); 
+        while(!parent.aborted() && ! child.aborted() && iter.hasNext()) {
+            IRunListener element = (IRunListener) iter.next();
+            element.addingChild(parent, child);
+        }
+       }
+}
diff --git a/testing/src/org/aspectj/testing/run/RunStatus.java b/testing/src/org/aspectj/testing/run/RunStatus.java
new file mode 100644 (file)
index 0000000..0a53080
--- /dev/null
@@ -0,0 +1,408 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.testing.util.BridgeUtil;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Default implementation of {@link IRunStatus}.
+ * @author isberg
+ */
+public class RunStatus implements IRunStatus {
+       private static int INDEX;
+
+       private final String name = "RunStatus[" + INDEX++ +"]";
+
+       /** true after isCompleted() evaluated true */
+       private boolean evaluated;
+
+       /** true after starting() called */
+       private boolean started; // set only in starting()
+
+       /** true after finished(int) or thrown(Throwable) called */
+       private boolean completed; // set only in completed(boolean)
+
+       /** contains any id set */
+       private Object id;
+
+       /** after finished(Object) called, contains that parameter */
+       private Object result;
+
+       /** after aborted(Object) called, contains that parameter */
+       private Object abortRequest;
+
+       /** use to set exception thrown, if any */
+       private Throwable thrown;
+
+       /** list of any messages submitted */
+       private IMessageHolder messageHolder;
+
+       /** list of any child status */
+       private ArrayList children;
+
+       /** parent of this status */
+       private IRunStatus parent;
+
+       /** invoker for any subruns */
+       private Runner runner;
+
+       /** controls runResult() */
+       private IRunValidator validator;
+
+//     public RunStatus() {
+//             reset();
+//             validator = RunValidator.NORMAL;
+//     }
+
+       public RunStatus(IMessageHolder holder, Runner runner) {
+               reset(holder, runner);
+               validator = RunValidator.NORMAL;
+       }
+
+       //------------------- process controls
+
+       /**
+        * Set identifier associated with this run, if any
+        * @throws IllegalArgumentException if id is null
+        * @throws IllegalStateException if id has already been set
+        */
+       public void setIdentifier(Object id) {
+               if (null == id) {
+                       throw new IllegalArgumentException("null id");
+               } else if ((null != this.id) && (id != this.id)) {
+                       throw new IllegalStateException(
+                               "attempt to set id " + this.id + " to " + id);
+               }
+               this.id = id;
+       }
+
+       /**
+        * Set  the current validator.
+        * @param delegate the RunValidatorI to use when calculating runStatus
+        * @throws IllegalArgumentException if delegate is null
+        */
+       public void setValidator(IRunValidator delegate) {
+               if (null == delegate) {
+                       throw new IllegalArgumentException("null delegate");
+               }
+               if (validator != delegate) {
+                       validator = delegate;
+               }
+       }
+
+       /**
+        * Call before any start() or after isCompleted() would return true
+        * to reset this to its pre-start state
+        * @throws IllegalStateException if start() has been called 
+        * and isCompleted() is not true.
+        */
+       public void reset() {
+               reset((IMessageHolder) null, (Runner) null);
+       }
+
+       /**
+        * Call before any start() or after isCompleted() would return true
+        * to reset this to its pre-start state.  Does not affect validator.
+        * @param holder the IMessageHolder to use after resetting.
+        * @throws IllegalStateException if start() has been called 
+        * and isCompleted() is not true.
+        */
+       public void reset(IMessageHolder holder, Runner runner) {
+        if (null == runner) {
+            throw new IllegalArgumentException("null runner");
+        }
+               if (started && (!isCompleted())) {
+                       throw new IllegalStateException("no reset() until isCompleted");
+               }
+               started = false;
+               completed = false;
+               result = null;
+               abortRequest = null;
+               thrown = null;
+               parent = null;
+               id = null;
+               messageHolder = (null != holder ? holder : new MessageHandler());
+               if (null != children) {
+                       children.clear();
+               }
+               this.runner = runner;
+               evaluated = false;
+       }
+
+       /** 
+        * Call only once to signal this run has started.
+        * @throws IllegalStateException if start() has been called
+        */
+       public void start() {
+               if (started) {
+                       throw new IllegalStateException("started already");
+               } else if (isCompleted()) {
+                       throw new IllegalStateException("start after completed (do reset)");
+               }
+               started = true;
+       }
+
+       /** 
+        * Call this or thrown only once after start() 
+        * to signal this run has ended.
+        * If this represents a void process, use VOID.
+        * @param result the Object returned by this run.
+        * @throws IllegalStateException if start() was not called first
+        *  or if either completed(Object) or thrown(Throwable) have been called.
+        */
+       public void finish(Object result) {
+               if (null == result) {
+                       throw new IllegalArgumentException("null result");
+               } else if (isCompleted()) {
+                       throw new IllegalStateException(
+                               "completed then finish " + result);
+               }
+               this.result = result;
+       }
+
+       /** 
+        * Call to signal this run is ending by request.
+        * If this represents a void process, use VOID.
+        * If there is no message, use ABORT.
+        * @param request the Object request to abort,
+        *         or ABORT if none is available.
+        * @throws IllegalStateException if start() was not called first
+        *  or if either completed(Object) or thrown(Throwable) have been called.
+        */
+       public void abort(Object request) {
+               if (null == request) {
+                       throw new IllegalArgumentException("null request");
+               } else if (isCompleted()) {
+                       throw new IllegalStateException(
+                               "completed then abort " + request);
+               }
+               this.abortRequest = request;
+       }
+
+       /** 
+        * Call this or completed only once after start() 
+        * to signal this run has ended.
+        * @throws IllegalStateException if start() was not called first
+        *  or if either completed(Object) or thrown(Throwable) have been called.
+        */
+       public void thrown(Throwable thrown) {
+               if (null == thrown) {
+                       throw new IllegalArgumentException("null thrown");
+               } else if (isCompleted()) {
+                       throw new IllegalStateException(
+                               "completed then thrown " + thrown);
+               }
+               this.thrown = thrown;
+       }
+
+       public void completeAbruptly() {
+               throw new Error("completing abruptly"); // XXX configurable
+       }
+
+       /**
+        * @return true if completed, not aborted, no thrown, no
+        * messages of kind ERROR, FAIL or ABORT, and
+        * result object is not IRunStatus.FAIL.
+        * @see org.aspectj.testing.harness.newbridge.IRunStatus#runResult()
+        */
+       public boolean runResult() {
+               return validator.runPassed(this);
+       }
+
+       //------------------- process messages
+       /** 
+        * Call this any time before isCompleted() would return true
+        * to signal any messages.
+        * @throws IllegalStateException if isCompleted().
+        */
+       public boolean handleMessage(IMessage message) {
+               return messageHolder.handleMessage(message);
+       }
+       public boolean isIgnoring(IMessage.Kind kind) {
+               return messageHolder.isIgnoring(kind);
+       }
+    
+    /**
+        * @see org.aspectj.bridge.IMessageHolder#hasAnyMessage(Kind, boolean)
+        */
+       public boolean hasAnyMessage(IMessage.Kind kind, boolean orGreater) {
+        return messageHolder.hasAnyMessage(kind, orGreater);
+       }
+
+    /**
+        * @see org.aspectj.bridge.IMessageHolder#getMessages(Kind)
+        */
+       public IMessage[] getMessages(IMessage.Kind kind, boolean orGreater) {
+               return messageHolder.getMessages(kind, orGreater);
+       }
+    
+    /**
+        * @see org.aspectj.bridge.IMessageHolder#numMessages(Kind)
+        */
+       public int numMessages(IMessage.Kind kind, boolean orGreater) {
+               return messageHolder.numMessages(kind, orGreater);
+       }
+
+       //------------------- process display
+       /** @return true if this run has started */
+       public boolean started() {
+               return started;
+       }
+
+       /** @return true if one of the result, abort request, or thrown is available */
+       public boolean isCompleted() {
+               if (!evaluated) {
+                       if (started
+                               && ((null != thrown)
+                                       || (null != result)
+                                       || (null != abortRequest))) {
+                               completed = true;
+                               evaluated = true;
+                       }
+               }
+               return completed;
+       }
+
+       /** @return true if this got an abort request */
+       public boolean aborted() {
+               return (completed && (null != abortRequest));
+       }
+
+       /** @return the Object result, if any, of this run */
+       public Object getResult() {
+               return result;
+       }
+
+       /** @return the Object abort request, if any, of this run */
+       public Object getAbortRequest() {
+               return abortRequest;
+       }
+
+       /** @return the Throwable thrown, if any, by this run */
+       public Throwable getThrown() {
+               return thrown;
+       }
+
+    /**
+     * @see org.aspectj.bridge.IMessageHolder#getUnmodifiableListView()
+     */
+    public List getUnmodifiableListView() {
+        return messageHolder.getUnmodifiableListView();
+    }
+    
+       /** @return any Message[] signalled, or IMessage.NONE if none */
+       public IMessage[] getMessages() {
+               return messageHolder.getMessages(null, IMessageHolder.EQUAL);
+       }
+
+       /** @return the identifier set for this run, if any */
+       public Object getIdentifier() {
+               return id;
+       }
+
+    /**
+        * @see org.aspectj.bridge.IMessageHolder#clearMessages()
+     * @throws UnsupportedOperationException always
+        */
+       public void clearMessages() throws UnsupportedOperationException {
+        throw new UnsupportedOperationException("use reset");
+       }
+
+       //------------------- subprocess
+
+       /** get the invoker for any subrunners */
+       public Runner getRunner() {
+               return runner;
+       }
+
+       /** 
+        * Add a record for a child run
+        * and install self as parent.
+        * @throws IllegalArgumentException if child is null
+        */
+       public void addChild(IRunStatus child) {
+               if (null == child) {
+                       throw new IllegalArgumentException("null child");
+               }
+               if (null == children) {
+                       children = new ArrayList();
+               }
+               children.add(child);
+       }
+
+       /**
+        * Register this as the run parent.
+        * (Any run that does addChild(IRunStatus) should register as parent.)
+        * @throws IllegalArgumentException if parent is null
+        * @throws IllegalStateException if parent exists already
+        */
+       public void registerParent(IRunStatus parent) {
+               if (null == parent) {
+                       throw new IllegalArgumentException("null parent");
+               } else if (null != this.parent) {
+                       throw new IllegalStateException(
+                               "adding parent " + parent + " to parent " + this.parent);
+               }
+               this.parent = parent;
+       }
+
+       /** 
+        * @return the current children of this run, or EMPTY_NEST if none
+        */
+       public IRunStatus[] getChildren() {
+               if ((null == children) || (0 == children.size())) {
+                       return EMPTY_NEST;
+               } else {
+                       return (IRunStatus[]) children.toArray(EMPTY_NEST);
+               }
+       }
+
+       /**
+        * @return the currently-registered parent, or null if none
+        */
+       public IRunStatus getParent() {
+               return parent;
+       }
+
+   public String toString() { 
+               return BridgeUtil.toShortString(this);
+   }
+
+   public String toLongString() { 
+        StringBuffer sb = new StringBuffer();
+        sb.append(BridgeUtil.toShortString(this));
+               if ((null != children) && (0 < children.size())) {
+                       String label = "### --------- " + name;
+                       int index = 0;
+                       for (Iterator iter = children.iterator(); iter.hasNext();) {
+                               IRunStatus childStatus = (IRunStatus) iter.next();
+                               String childLabel =
+                                       "\n" + label + " child[" + index++ +"] " 
+                    + childStatus.getIdentifier();
+                               sb.append(childLabel + " ---- start\n");
+                               sb.append(childStatus.toString());
+                               sb.append(childLabel + " ---- end\n");
+                       }
+               }
+               return sb.toString();
+       }
+}
diff --git a/testing/src/org/aspectj/testing/run/RunValidator.java b/testing/src/org/aspectj/testing/run/RunValidator.java
new file mode 100644 (file)
index 0000000..9cf91e3
--- /dev/null
@@ -0,0 +1,206 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.testing.util.IntRange;
+import org.aspectj.testing.util.ObjectChecker;
+
+/**
+ * This checks if a run status passes, as follows:
+ * <li>state: fail unless completed and not aborted</li>
+ * <li>messages: fail if any of type ABORT, FAIL - permit ERROR, WARNING, DEBUG...
+ *     (which permits expected compiler errors and warnings) </li>
+ * <li>thrown: if type required, fail unless type thrown; 
+ *             if type permitted, fail if wrong type thrown</li>
+ * <li>result: fail unless no ObjectChecker or it validates
+ *              and the result is not IRunStatus.FAIL.</li>
+ * <li>otherwise delegates to any subclass doPassed()<li>
+ * Client setup the expected and permitted exception classes
+ * and the result object checker, and may also subclass to
+ * query the IRunStatus more carefully.
+ * <p>
+ * Note that IRunStatus states can be out of sync with messages,
+ * e.g., as underlying components signal ABORT without using abort(...).
+ */
+public class RunValidator implements IRunValidator {
+    /** expect normal completion with any non-null result object */
+    public static final IRunValidator NORMAL
+            = new RunValidator(ObjectChecker.ANY);
+
+    /** expect normal completion and Integer result object with value 0 */
+    public static final IRunValidator ZERO_STATUS
+            = new RunValidator(IntRange.ZERO);
+
+    /** expect finished(IRunStatus.PASS) and no thrown, fail, etc. */
+    public static final IRunValidator PASS 
+            = new RunValidator(new ObjectChecker() {
+                public boolean isValid(Object o) {
+                    return (o == IRunStatus.PASS);
+                }
+            });
+            
+    /** expect finished(IRunStatus.FAIL) */
+    public static final IRunValidator FAIL 
+            = new RunValidator(new ObjectChecker() {
+                public boolean isValid(Object o) {
+                    return (o == IRunStatus.FAIL);
+                }
+            });
+
+    /** range of status values required for passing */
+    private ObjectChecker resultChecker;
+    
+    // XXX replace two exc. classes with one, plus boolean for how to interpret?
+    /** if non-null, passed() permits any thrown assignable to this class */
+    private Class permittedExceptionsClass;
+
+    /** if non-null, passed() requires some thrown assignable to this class */
+    private Class requiredExceptionsClass;
+    
+    /** Create result validator that expects a certain int status */
+    public RunValidator(ObjectChecker resultChecker) {
+       this(resultChecker, null, null);
+    }   
+     
+    /** 
+     * Create result validator that passes only when completed abruptly by
+     * a Throwable assignable to the specified class.
+     * @throws illegalArgumentException if requiredExceptionsClass is not Throwable
+     */
+    public RunValidator(Class requiredExceptionsClass) { 
+        this(null, null, requiredExceptionsClass);
+    }
+
+    /**
+     * Create a result handler than knows how to evaluate {@link #passed()}.
+     * You cannot specify both permitted and required exception class,
+     * and any exception class specified must be assignable to throwable.
+     * 
+     * @param resultChecker  {@link #passed()} will return false if
+     * the int status is not accepted by this int validator - if null,
+     * any int status result is accepted.
+     * @param fastFailErrorClass an Error subclass with a (String) constructor to use to 
+     *                            construct and throw Error from fail(String).  If null, then fail(String)
+     *                            returns normally.
+     * @param permittedExceptionsClass if not null and any exceptions thrown are
+     * assignable to this class, {@link #passed()} will not return
+     * false as it normally does when exceptions are thrown.
+     * @param requiredExceptionsClass if not null,  {@link #passed()} will return false
+     * unless some exception was thrown that is assignable to this class.
+     * @throws illegalArgumentException if any exception class is not Throwable
+     *          or if fast fail class is illegal (can't make String constructor)
+     */
+    protected RunValidator(
+        ObjectChecker resultChecker,
+        Class permittedExceptionsClass,
+        Class requiredExceptionsClass) {
+        init(resultChecker,permittedExceptionsClass, requiredExceptionsClass);
+    }
+
+    /** same as init with existing values */
+    protected void reset() {
+        init(resultChecker, permittedExceptionsClass,
+         requiredExceptionsClass);
+    }
+    
+    /** subclasses may use this to re-initialize this for re-use */
+    protected void init(
+        ObjectChecker resultChecker,
+        Class permittedExceptionsClass,
+        Class requiredExceptionsClass) {
+        this.permittedExceptionsClass = permittedExceptionsClass;
+        this.requiredExceptionsClass = requiredExceptionsClass;
+        
+        if (null != resultChecker) {
+            this.resultChecker = resultChecker;
+        } else {
+            this.resultChecker = IntRange.ANY;
+        }
+
+        if (null != permittedExceptionsClass) {
+           if (!Throwable.class.isAssignableFrom(permittedExceptionsClass)) {
+                String e = "permitted not throwable: " + permittedExceptionsClass;
+                throw new IllegalArgumentException(e);
+           } 
+        }
+        if (null != requiredExceptionsClass) {
+           if (!Throwable.class.isAssignableFrom(requiredExceptionsClass)) {
+                String e = "required not throwable: " + requiredExceptionsClass;
+                throw new IllegalArgumentException(e);
+           } 
+        }
+        if ((null != permittedExceptionsClass) 
+            && (null != requiredExceptionsClass) ) {
+            String e = "define at most one of required or permitted exceptions";
+            throw new IllegalArgumentException(e);
+        }
+    }
+    
+    /** @return true if this result passes per this validator */
+    public final boolean runPassed(IRunStatus result) {
+        if (null == result) {
+            throw new IllegalArgumentException("null result");
+        }
+        // After the result has completed, the result is stored.
+        if (!result.isCompleted()) {
+            return false;
+        }
+        if (result.aborted()) {
+            return false;
+        }
+        if (null != result.getAbortRequest()) {
+            return false;
+        }
+        Object resultObject = result.getResult();
+        if (!resultChecker.isValid(resultObject)) {
+            return false;
+        }
+        if (resultObject == IRunStatus.FAIL) {
+             return false;
+        }
+        // need MessageHandler.getMessage(...)
+        if (result.hasAnyMessage(IMessage.FAIL, IMessageHolder.ORGREATER)) {
+            return false;
+        }
+        Throwable thrown = result.getThrown();
+        if (null == thrown) {
+            if (null != requiredExceptionsClass) {
+                return false;
+            }
+        } else {
+            Class c = thrown.getClass();
+            // at most one of the ExceptionsClass set
+            if (null != requiredExceptionsClass) {
+                if (!requiredExceptionsClass.isAssignableFrom(c)) {
+                  return false;
+                }
+            } else if (null != permittedExceptionsClass) {
+                if (!permittedExceptionsClass.isAssignableFrom(c)) {
+                    return false;
+                }
+            } else {
+                return false;
+            }            
+        }
+        return dopassed();
+    }
+
+    /** subclasses implement subclass-specific behavior for passed() here */
+    protected boolean dopassed() {
+        return true;
+    }
+}
diff --git a/testing/src/org/aspectj/testing/run/Runner.java b/testing/src/org/aspectj/testing/run/Runner.java
new file mode 100644 (file)
index 0000000..6cb9e75
--- /dev/null
@@ -0,0 +1,510 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.Message;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.util.LangUtil;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+/**
+ * Run IRun, setting status and invoking listeners
+ * for simple and nested runs.
+ * <p>
+ * This manages baseline IRun status reporting: 
+ * any throwables are caught and reported, and
+ * the status is started before running and 
+ * (if not already completed) completed after.
+ * <p>
+ * This runs any IRunListeners specified directly in the 
+ * run*(..., IRunListener) methods
+ * as well as any specified indirectly by registering listeners per-type
+ * in {@link registerListener(Class, IRunListener)} 
+ * <p>
+ * For correct handling of nested runs, this sets up
+ * status parent/child relationships.
+ * It uses the child result object supplied directly in the
+ * runChild(..., IRunStatus childStatus,..) methods,
+ * or (if that is null) one obtained from the child IRun itself, 
+ * or (if that is null) a generic IRunStatus.  
+ * <p>
+ * For IRunIterator, this uses IteratorWrapper to wrap the 
+ * iterator as an IRun.  Runner and IteratorWrapper coordinate
+ * to handle fast-fail (aborting further iteration when an IRun fails).
+ * The IRunIterator itself may specify fast-fail by returning true
+ * from {@link IRunIterator#abortOnFailure()}, or clients can 
+ * register IRunIterator by Object or type for fast-failure using
+ * {@link registerFastFailIterator(IRunIterator)} or 
+ * {@link registerFastFailIterator(Class)}.
+ * This also ensures that 
+ * {@link IRunIterator#iterationCompleted()} is
+ * called after the iteration process has completed.
+ */
+public class Runner {
+    // XXX need to consider wiring in a logger - sigh
+
+    private static final IMessage FAIL_NORUN
+        = MessageUtil.fail("Null IRun parameter to Runner.run(IRun..)");
+    private static final IMessage FAIL_NORUN_ITERATOR
+        = MessageUtil.fail("Null IRunterator parameter to Runner.run(IRunterator...)");
+    
+    public Runner() {
+    }
+    
+    /**
+     * Do the run, setting run status, invoking
+     * listener, and aborting as necessary.  
+     * If the run is null, the status is
+     * updated, but the listener is never run.  
+     * If the listener is null, then the runner does a lookup
+     * for the listeners of this run type.
+     * Any exceptions thrown by the listener(s) are added
+     * to the status messages and processing continues. 
+     * unless the status is aborted.
+     * The status will be completed when this method completes.
+     * @param run the IRun to run - if null, issue a FAIL message
+     * to that effect in the result.
+     * @throws IllegalArgumentException if status is null
+     * @return boolean result returned from IRun 
+     * or false if IRun did not complete normally
+     * or status.runResult() if aborted.
+     */
+    /* XXX later permit null status
+     * If the status is null, this tries to complete
+     * the run without a status.  It ignores exceptions
+     * from the listeners, but does not catch any from the run.
+     */
+    public boolean run(IRun run, IRunStatus status, 
+                             IRunListener listener) {
+        return run(run, status, listener, (Class) null);
+    }  
+                                                 
+    public boolean run(IRun run, IRunStatus status, 
+                             IRunListener listener, Class exceptionClass) {                                
+        if (!precheck(run, status)) {
+            return false;
+        }
+        RunListeners listeners = getListeners(run, listener);
+        return runPrivate(run, status, listeners, exceptionClass);
+                             
+    }
+
+    /**
+     * Run child of parent, handling interceptor registration, etc.
+     * @throws IllegalArgumentException if parent or child status is null
+     */
+    public boolean runChild(IRun child, 
+                                 IRunStatus parentStatus, 
+                                 IRunStatus childStatus,
+                                 IRunListener listener) {
+        return runChild(child, parentStatus, childStatus, listener, null);
+    }
+    
+    /**
+     * Run child of parent, handling interceptor registration, etc.
+     * If the child run is supposed to throw an exception, then pass
+     * the exception class.
+     * After this returns, the childStatus is guaranteed to be completed.
+     * If an unexpected exception is thrown, an ABORT message 
+     * is passed to childStatus.
+     * @param parentStatus the IRunStatus for the parent - must not be null
+     * @param childStatus the IRunStatus for the child - default will be created if null
+     * @param exceptionClass the Class of any expected exception
+     * @throws IllegalArgumentException if parent status is null
+     */
+    public boolean runChild(IRun child, 
+                                 IRunStatus parentStatus, 
+                                 IRunStatus childStatus,
+                                 IRunListener listener,
+                                 Class exceptionClass) {
+        if (!precheck(child, parentStatus)) {
+            return false;
+        }
+        if (null == childStatus) {
+            childStatus = new RunStatus(new MessageHandler(), this);
+        }
+        installChildStatus(child, parentStatus, childStatus);
+        if (!precheck(child, childStatus)) {
+            return false;
+        }
+        RunListeners listeners = getListeners(child, listener);
+        if (null != listeners) {
+            try {
+                listeners.addingChild(parentStatus, childStatus);
+            } catch (Throwable t) {
+                String m = "RunListenerI.addingChild(..) exception " + listeners;
+                parentStatus.handleMessage(MessageUtil.abort(m, t)); // XXX                
+            }
+        }
+        boolean result = false;
+        try {
+            result = runPrivate(child, childStatus, listeners, exceptionClass);
+        } finally {
+            if (!childStatus.isCompleted()) {
+                childStatus.finish(result ? IRunStatus.PASS : IRunStatus.FAIL);
+                childStatus.handleMessage(MessageUtil.debug("XXX parent runner set completion"));
+            }
+        }
+        boolean childResult = childStatus.runResult();
+        if (childResult != result) {
+            childStatus.handleMessage(MessageUtil.info("childResult != result=" + result));
+        }
+        return childResult;        
+    }
+
+    public IRunStatus makeChildStatus(IRun run, IRunStatus parent, IMessageHolder handler) {
+        return installChildStatus(run, parent, new RunStatus(handler, this));
+    }
+    
+    /**
+     * Setup the child status before running
+     * @param run the IRun of the child process (not null)
+     * @param parent the IRunStatus parent of the child status (not null)
+     * @param child the IRunStatus to install - if null, a generic one is created
+     * @return the IRunStatus child status (as passed in or created if null)
+     */
+    public IRunStatus installChildStatus(IRun run, IRunStatus parent, IRunStatus child) {
+        if (null == parent) {
+            throw new IllegalArgumentException("null parent");
+        }
+        if (null == child) {
+            child = new RunStatus(new MessageHandler(), this);
+        }
+        child.setIdentifier(run); // XXX leak if ditching run...
+        parent.addChild(child);
+        return child;
+    }
+                  
+    /**
+     * Do a run by running all the subrunners provided by the iterator,
+     * creating a new child status of status for each.
+     * If the iterator is null, the result is
+     * updated, but the interceptor is never run.  
+     * @param iterator the IRunteratorI for all the IRun to run
+     * - if null, abort (if not completed) or message to status.
+     * @throws IllegalArgumentException if status is null
+     */
+    public boolean runIterator(IRunIterator iterator, IRunStatus status,
+                             IRunListener listener) {
+        LangUtil.throwIaxIfNull(status, "status");                       
+        if (status.aborted()) {
+            return status.runResult();
+        }
+        if (null == iterator) {
+            if (!status.isCompleted()) {
+                status.abort(IRunStatus.ABORT_NORUN);
+            } else {
+                status.handleMessage(FAIL_NORUN);
+            }                       
+            return false;
+        }
+        IRun wrapped = wrap(iterator, listener);
+        return run(wrapped, status, listener);
+    }
+
+    /**
+     * Signal whether to abort on failure for this run and iterator,
+     * based on the iterator and any fast-fail registrations.
+     * @param iterator the IRunIterator to stop running if this returns true
+     * @param run the IRun that failed
+     * @return true to halt further iterations
+     */
+    private boolean abortOnFailure(IRunIterator iterator, IRun run) {
+        return ((null == iterator) || iterator.abortOnFailure()); // XxX not complete
+    }
+
+
+    /** 
+     * Tell Runner to stop iterating over IRun for an IRunIterator
+     * if any IRun.run(IRunStatus) fails.
+     * This overrides a false result from IRunIterator.abortOnFailure().
+     * @param iterator the IRunIterator to fast-fail - ignored if null.
+     * @see IRunIterator#abortOnFailure()
+     */
+    public void registerFastFailIterator(IRunIterator iterator) { // XXX unimplemented
+        throw new UnsupportedOperationException("ignoring " + iterator);
+    }
+
+    /** 
+     * Tell Runner to stop iterating over IRun for an IRunIterator
+     * if any IRun.run(IRunStatus) fails, 
+     * if the IRunIterator is assignable to iteratorType.
+     * This overrides a false result from IRunIterator.abortOnFailure().
+     * @param iteratorType the IRunIterator Class to fast-fail
+     *              - ignored if null, must be assignable to IRunIterator
+     * @see IRunIterator#abortOnFailure()
+     */
+    public void registerFastFailIteratorTypes(Class iteratorType) { // XXX unimplemented
+        throw new UnsupportedOperationException("ignoring " + iteratorType);
+    }
+    
+    /**
+     * Register a run listener for any run assignable to type.
+     * @throws IllegalArgumentException if either is null
+     */
+    public void registerListener(Class type, IRunListener listener) { // XXX unregister
+        if (null == type) {
+            throw new IllegalArgumentException("null type");
+        }
+        if (null == listener) {
+            throw new IllegalArgumentException("null listener");
+        }
+        ClassListeners.addListener(type, listener);
+    }
+    
+    /**
+     * Wrap this IRunIterator.
+     * This wrapper takes care of calling 
+     * <code>iterator.iterationCompleted()</code> when done, so
+     * after running this, clients should not invoker IRunIterator 
+     * methods on this iterator.
+     * @return the iterator wrapped as a single IRun
+     */
+    public IRun wrap(IRunIterator iterator, IRunListener listener) {
+        LangUtil.throwIaxIfNull(iterator, "iterator");
+        return new IteratorWrapper(this, iterator, listener);
+    }                             
+        
+    /**
+     * This gets any listeners registered for the run
+     * based on the class of the run (or of the wrapped iterator,
+     * if the run is an IteratorWrapper).
+     * @return a listener with all registered listener and parm,
+     * or null if parm is null and there are no listeners
+     */
+    protected RunListeners getListeners(IRun run, IRunListener listener) {        
+        if (null == run) {
+            throw new IllegalArgumentException("null run");
+        }
+        Class runClass = run.getClass();
+        if (runClass == IteratorWrapper.class) {
+            IRunIterator it = ((IteratorWrapper) run).iterator;
+            if (null != it) {
+                runClass = it.getClass();
+            }
+            // fyi expecting: listener == ((IteratorWrapper) run).listener
+        }
+        RunListeners listeners = ClassListeners.getListeners(runClass);
+        if (null != listener) {
+            listeners.addListener(listener);
+        }
+        return listeners; // XXX implement registration
+    }
+
+    /** check status and run before running */    
+    private boolean precheck(IRun run, IRunStatus status) {
+        if (null == status) {
+            throw new IllegalArgumentException("null status");
+        }
+        // check abort request coming in
+        if (status.aborted()) {
+            return status.runResult();
+        } else if (status.isCompleted()) {
+            throw new IllegalStateException("status completed before starting");
+        }
+        
+        if (!status.started()) {
+            status.start();
+        }
+        return true;
+    }
+        
+    /** This assumes precheck has happened and listeners have been obtained */
+    private boolean runPrivate(IRun run, IRunStatus status, 
+                             RunListeners listeners, Class exceptionClass) {
+        IRunListener listener = listeners;                               
+        if (null != listener) {
+            try {
+                listener.runStarting(status);
+            } catch (Throwable t) {
+                String m = listener + " RunListenerI.runStarting(..) exception";
+                IMessage mssg = new Message(m, IMessage.WARNING, t, null); 
+                // XXX need IMessage.EXCEPTION - WARNING is ambiguous
+                status.handleMessage(mssg);
+            }
+        }
+        // listener can set abort request
+        if (status.aborted()) {
+            return status.runResult();
+        }
+        if (null == run) {
+            if (!status.isCompleted()) {
+                status.abort(IRunStatus.ABORT_NORUN);
+            } else {
+                status.handleMessage(MessageUtil.FAIL_INCOMPLETE);
+            }
+            return false;
+        } 
+        
+        boolean result = false;
+        try {
+            result = run.run(status);
+            if (!status.isCompleted()) {                
+                status.finish(result?IRunStatus.PASS: IRunStatus.FAIL); 
+            }
+        } catch (Throwable thrown) {
+            if (!status.isCompleted()) {
+                status.thrown(thrown);
+            } else {
+                String m = "run status completed but run threw exception";
+                status.handleMessage(MessageUtil.abort(m, thrown));
+                result = false;
+            }
+        } finally {
+            if (!status.isCompleted()) {
+                // XXX should never get here... - hides errors to set result
+                status.finish(result ? IRunStatus.PASS : IRunStatus.FAIL);
+                if (!status.isCompleted()) {
+                    status.handleMessage(MessageUtil.debug("child set of status failed"));
+                }
+            }
+        }
+        
+        
+        try {
+            if ((null != listener) && !status.aborted()) {
+                listener.runCompleted(status);
+            }
+        } catch (Throwable t) {
+            String m = listener + " RunListenerI.runCompleted(..) exception";
+            status.handleMessage(MessageUtil.abort(m, t));
+        }
+        return result;
+    }
+
+    //---------------------------------- nested classes
+    /** 
+     * Wrap an IRunIterator as a IRun, coordinating 
+     * fast-fail IRunIterator and Runner
+     */ 
+    public static class IteratorWrapper implements IRun {
+        final Runner runner;
+        public final IRunIterator iterator;
+        final IRunListener listener;
+        
+        public IteratorWrapper(Runner runner, IRunIterator iterator, IRunListener listener) {
+            LangUtil.throwIaxIfNull(iterator, "iterator");
+            LangUtil.throwIaxIfNull(runner, "runner");
+            this.runner = runner;
+            this.iterator = iterator;
+            this.listener = listener;
+        }
+
+        /** @return null */
+        public IRunStatus makeStatus() { 
+            return null; 
+        }
+        
+        /** @return true unless one failed */
+        public boolean run(IRunStatus status) {
+            boolean result = true;
+            try {
+                int i = 0;
+                int numMessages = status.numMessages(IMessage.FAIL, IMessageHolder.ORGREATER);
+                while (iterator.hasNextRun()) {
+                    IRun run = iterator.nextRun((IMessageHandler) status, runner);
+                    if (null == run) {
+                        MessageUtil.debug(status,  "null run " + i + " from " + iterator);
+                        continue;
+                    }
+    
+                    int newMessages = status.numMessages(IMessage.FAIL, IMessageHolder.ORGREATER);
+                    if (newMessages > numMessages) {
+                        numMessages = newMessages;
+                        String m = "run " + i + " from " + iterator
+                            + " not invoked, due to fail(+) message(s) ";
+                        MessageUtil.debug(status,  m);
+                        continue;
+                    }
+                    RunStatus childStatus = null; // let runChild create
+                    if (!runner.runChild(run, status, childStatus, listener)) {
+                        if (result) {
+                            result = false;
+                        }
+                        if (iterator.abortOnFailure() 
+                            || runner.abortOnFailure(iterator, run)) {
+                            break;
+                        }
+                    }
+                    i++;
+                }
+                return result;
+            } finally {
+                iterator.iterationCompleted();
+            }
+        }
+
+        /** @return iterator, clipped to 75 char */
+        public String toString() {
+            String s = "" + iterator;
+            if (s.length() > 75) {
+                s = s.substring(0, 72) + "...";
+            }
+            return s;
+        }
+    }
+    
+    /** per-class collection of IRun */
+    static class ClassListeners extends RunListeners {        
+        private static final Hashtable known = new Hashtable();
+        
+        static RunListeners getListeners(Class c) { // XXX costly and stupid
+            Enumeration keys = known.keys();
+            RunListeners many = new RunListeners();
+            while (keys.hasMoreElements()) {
+                Class kc = (Class) keys.nextElement();
+                if (kc.isAssignableFrom(c)) {
+                    many.addListener((IRunListener) known.get(kc));
+                }
+            }
+            return many;
+        }
+        
+        private static RunListeners makeListeners(Class c) {
+            RunListeners result = (ClassListeners) known.get(c);
+            if (null == result) {
+                result = new ClassListeners(c);
+                known.put(c, result);
+            }
+            return result;
+        }
+        
+        static void addListener(Class c, IRunListener listener) {
+            if (null == listener) {
+                throw new IllegalArgumentException("null listener");
+            }
+            if (null == c) {
+                c = IRun.class;
+            }
+            makeListeners(c).addListener(listener);
+        }
+        
+        Class clazz;
+        
+        ClassListeners(Class clazz) {
+            this.clazz = clazz;
+        }
+        
+        public String toString() {
+            return clazz.getName() + " ClassListeners: " + super.toString();
+        }
+    }       
+}
diff --git a/testing/src/org/aspectj/testing/run/WrappedRunIterator.java b/testing/src/org/aspectj/testing/run/WrappedRunIterator.java
new file mode 100644 (file)
index 0000000..e8c8bc0
--- /dev/null
@@ -0,0 +1,72 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.run;
+
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.util.LangUtil;
+
+/** Adapt IRun to IRunIterator in a way that can be detected for hoisting. */
+public class WrappedRunIterator implements IRunIterator {
+    protected final Object id;
+    protected IRun run;
+    
+    /**
+     * @param id the Object used for toString(), if set
+     * @param run the IRun returned from the first call to 
+     * nextRun(IMessageHandler handler, Runner runner)
+     */
+    public WrappedRunIterator(Object id, IRun run) {
+        LangUtil.throwIaxIfNull(run, "run");
+        this.id = id;
+        this.run = run;
+    }
+
+    /** @return false always - we run only once anyway */
+    public boolean abortOnFailure() {
+        return false;
+    }
+
+    /**
+     * @return true until nextRun() completes
+     * @see org.aspectj.testing.run.RunIteratorI#hasNextRun()
+     */
+    public boolean hasNextRun() {
+        return (null != run);
+    }
+
+    /**
+        * @see org.aspectj.testing.run.IRunIterator#iterationCompleted()
+        */
+       public void iterationCompleted() {
+       }
+
+    /**
+     * @return the only IRun we have, and null thereafter
+     * @see org.aspectj.testing.run.RunIteratorI#nextRun(IMessageHandler, Runner)
+     */
+    public IRun nextRun(IMessageHandler handler, Runner runner) {
+        if (null == run) {
+            return null;
+        } else {
+            IRun result = run;
+            run = null;
+            return result;
+        }
+    }
+
+    /** @return name */
+    public String toString() {
+        return (null == id ? run : id).toString();
+    }        
+}
diff --git a/testing/src/org/aspectj/testing/util/AccumulatingFileFilter.java b/testing/src/org/aspectj/testing/util/AccumulatingFileFilter.java
new file mode 100644 (file)
index 0000000..5ef0ecf
--- /dev/null
@@ -0,0 +1,53 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.util.Vector;
+
+/** 
+ * A FileFilter that accumulates the results when called if they exist.
+ * Subclasses override accumulate to determine whether it should be
+ * accumulated.
+ */
+public class AccumulatingFileFilter extends ValidFileFilter {
+    Vector files = new Vector();
+    public final boolean accept(File f) {
+        if (super.accept(f) && (accumulate(f))) {
+            files.add(f);
+        }
+        return true;
+    }
+
+    /** 
+     * This implementation accumulates everything.
+     * Subclasses should override to implement filter
+     * @param file a File guaranteed to exist 
+     * @return true if file should be accumulated. 
+     */
+    public boolean accumulate(File f) {
+        return true;
+    }
+    /**
+     * @return list of files currently accumulated 
+     */
+    public File[] getFiles() {
+        int numFiles = files.size();
+        File[] result = new File[numFiles];
+        if (0 < numFiles) {
+            files.copyInto(result);
+        }
+        return result;
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/BridgeUtil.java b/testing/src/org/aspectj/testing/util/BridgeUtil.java
new file mode 100644 (file)
index 0000000..8e4b60f
--- /dev/null
@@ -0,0 +1,439 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.Message;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.bridge.SourceLocation;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.RunValidator;
+import org.aspectj.util.FileUtil;
+import org.aspectj.util.LineReader;
+
+import java.io.File;
+import java.util.Comparator;
+
+/**
+ * 
+ */
+public class BridgeUtil {
+
+    private static final String INDENT = "    ";
+
+    /** result value when writeMessage is passed null */
+    private static final String NULL_MESSAGE_OUTPUT = "<null message output>";
+    
+    /** result value when readMessage is passed null */
+    private static final IMessage NULL_MESSAGE_INPUT = null;
+    
+    private static final String KIND_DELIM = ": \"";
+    private static final String MESSAGE_DELIM = "\" - ";
+
+    
+    public static ISourceLocation makeSourceLocation(LineReader reader) {
+        LangUtil.throwIaxIfNull(reader, "reader");
+        int line = reader.getLineNumber();
+        
+        return new SourceLocation(reader.getFile(), line, line, 0);
+    }
+
+
+    /**
+     * Method readSourceLocation.
+     * @param sourceLocStr
+     * @return ISourceLocation
+     */
+    private static ISourceLocation readSourceLocation(String sourceLocStr) {
+        return BridgeUtil.makeSourceLocation(sourceLocStr);
+    }
+    public static IMessage makeMessage(String message, IMessage.Kind kind, 
+                                       Throwable thrown, LineReader reader) {
+        ISourceLocation sl = (null == reader ? null : MessageUtil.makeSourceLocation(reader));
+        if (null == kind) kind = IMessage.INFO;
+        return new Message(message, kind, thrown, sl);        
+    }
+
+    /**
+     * Read a message from a string written by writeMessage(IMessage).
+     * Does not handle exceptions at all or source location well. XXX
+     * @param message the String representation of a message
+     * @return IMessage
+     */
+    public static IMessage readMessage(String message) {
+       if (null == message) {
+            return NULL_MESSAGE_INPUT;
+       }
+       if (NULL_MESSAGE_OUTPUT.equals(message)) {
+            return null;
+       }
+       int kindEnd = message.indexOf(KIND_DELIM);
+       int messageEnd = message.indexOf(MESSAGE_DELIM);
+       int messageStart = kindEnd+KIND_DELIM.length();
+       int sourceLocStart = messageEnd+MESSAGE_DELIM.length();
+       String kindStr = message.substring(0, kindEnd);
+       String text = message.substring(messageStart, messageEnd);
+       String sourceLocStr = message.substring(sourceLocStart);
+       IMessage.Kind kind = MessageUtil.getKind(kindStr);
+       ISourceLocation loc = readSourceLocation(sourceLocStr);
+       return new Message(text, kind, null, loc);
+    }
+    
+
+    /**
+     * Write a message to a string to be read by readMessage(String)
+     * @param message the String representation of a message
+     * @return IMessage
+     */
+    public static String writeMessage(IMessage message) {
+        if (null == message) {
+            return NULL_MESSAGE_OUTPUT;
+        }
+        return message.getKind() 
+            + KIND_DELIM 
+            + message.getMessage() 
+            + MESSAGE_DELIM
+            + message.getISourceLocation(); // XXX implement
+    }
+    
+
+    public static class Comparators {        
+        /** 
+         * This is a weak ordering
+         * so use only for sorts, not to maintain maps.
+         * It returns 0 if one file path is a suffix of the other
+         * or a case-insensitive string comparison otherwise.
+         */
+        public static final Comparator WEAK_File = new Comparator() {
+            public int compare(Object o1, Object o2) {
+                File one = (File) o1;
+                File two = (File) o2;
+                if (null == one) {
+                    return (null == two ? 0 : 1);
+                } else if (null == two) {
+                    return -1;
+                } else if (one == two) {
+                    return 0;
+                } else {
+                    String s1 = FileUtil.weakNormalize(one.getPath());
+                    String s2 = FileUtil.weakNormalize(two.getPath());
+                    if (s1.endsWith(s2) || s2.endsWith(s1)) {
+                        return 0;
+                    } else {
+                        return String.CASE_INSENSITIVE_ORDER.compare(s1, s2);
+                    }
+                }
+        }};
+        /** 
+         * Ordering ignores filename,
+         * so use only for sorts, not to maintain maps
+         */
+        public static final Comparator WEAK_ISourceLocation = new Comparator() {
+            public int compare(Object o1, Object o2) {
+                ISourceLocation one = (ISourceLocation) o1;
+                ISourceLocation two = (ISourceLocation) o2;
+                if (null == one) {
+                    return (null == two ? 0 : 1);
+                } else if (null == two) {
+                    return -1;
+                } else if (one == two) {
+                    return 0;
+                } else { // XXX start with File to make strong
+                    int i1 = one.getLine();
+                    int i2 = two.getLine(); 
+                    return i1 - i2;
+//                    // defer subcomparisons until messages complete                                       
+//                    if (i1 != i2) {
+//                        return i1 - i2;
+//                    } else {
+//                        i1 = one.getColumn();
+//                        i2 = two.getColumn();
+//                        if (i1 != i2) {
+//                            return i1 - i2;
+//                        } else {
+//                            i1 = one.getEndLine();
+//                            i2 = two.getEndLine();
+//                            return i1 - i2;
+//                        }
+//                    }
+                }
+        }};
+
+        /** 
+         * Ordering ignores filename and message
+         * so use only for sorts, not to maintain maps
+         */
+        public static final Comparator WEAK_IMessage = new Comparator() {
+            public int compare(Object o1, Object o2) {
+                IMessage one = (IMessage) o1;
+                IMessage two = (IMessage) o2;
+                if (null == one) {
+                    return (null == two ? 0 : 1);
+                } else if (null == two) {
+                    return -1;
+                } else if (one == two) {
+                    return 0;
+                } else {
+                    IMessage.Kind kind1 = one.getKind();
+                    IMessage.Kind kind2= two.getKind();
+                    if (kind1 != kind2) {
+                        return IMessage.Kind.COMPARATOR.compare(kind1, kind2);
+                    } else {
+                        ISourceLocation sl1 = one.getISourceLocation();
+                        ISourceLocation sl2 = two.getISourceLocation();
+                        return WEAK_ISourceLocation.compare(sl1, sl2);
+                    }
+                }
+        }};
+        
+    }
+
+    public static SourceLocation makeSourceLocation(String input) { // XXX only for testing, not production
+        return makeSourceLocation(input, (File) null);
+    }
+    
+    public static SourceLocation makeSourceLocation(String input, String path) { 
+        return makeSourceLocation(input, (null == path ? null : new File(path)));        
+    }
+    
+       /** attempt to create a source location from the input */
+    public static SourceLocation makeSourceLocation(String input, File defaultFile) { 
+    /*
+     * Forms interpreted:
+     * # - line
+     * file - file
+     * file:# - file, line
+     * #:# - if defaultFile is not null, then file, line, column
+     * file:#:# - file, line, column
+     * file:#:#:? - file, line, column, message
+     */
+        SourceLocation result = null;
+        if ((null == input) || (0 == input.length())) {
+            if (null == defaultFile) {
+                return null;
+            } else {
+                return new SourceLocation(defaultFile, 0, 0, 0);
+            }
+        }
+        input = input.trim();
+        
+        String path = null;
+        int line = 0;
+        int endLine = 0;
+        int column = 0;
+        String message = null;
+        
+        // first try line only
+        line = convert(input);
+        if (-1 != line) {
+            return new SourceLocation(defaultFile, line, line, 0);
+        }
+        
+        // if not a line - must be > 2 characters
+        if (3 > input.length()) {
+            return null; // throw new IllegalArgumentException("too short: " + input);
+        }
+        final String fixTag = "FIXFIX";
+        if (input.charAt(1) == ':') { // windows drive ambiguates ":" file:line:col separator
+            input = fixTag + input.substring(0,1) + input.substring(2);
+        }
+        // expecting max: path:line:column:message
+        // if 1 colon, delimits line (to second colon or end of string)
+        // if 2 colon, delimits column (to third colon or end of string)
+        // if 3 colon, delimits column (to fourth colon or end of string)
+        // todo: use this instead??
+        final int colon1 = input.indexOf(":",2); // 2 to get past windows drives...
+        final int colon2 = (-1 == colon1?-1:input.indexOf(":", colon1+1));
+        final int colon3 = (-1 == colon2?-1:input.indexOf(":", colon2+1));
+        String s;
+        if (-1 == colon1) {      // no colon; only path (number handled above)
+            path = input;
+        } else {                 // 1+ colon => file:line // XXX later or line:column
+            path = input.substring(0, colon1);
+            s = input.substring(colon1+1,(-1!=colon2?colon2:input.length())).trim();
+            line = convert(s);
+            if (-1 == line) {
+               return null;
+               //line = "expecting line(number) at \"" + line + "\" in " + input;
+               //throw new IllegalArgumentException(line);
+            } else if (-1 != colon2) { // 2+ colon => col
+                s = input.substring(colon2+1,(-1!=colon3?colon3:input.length())).trim();
+                column = convert(s);
+                if (-1 == column) {
+                   return null;
+                   //col = "expecting col(number) at \"" + col + "\" in " + input;
+                   //throw new IllegalArgumentException(col);
+                } else if (-1 != colon3) { // 3 colon => message
+                    message = input.substring(colon3+1); // do not trim message
+                }
+            }
+        }
+
+        if (path.startsWith(fixTag)) {
+            int len = fixTag.length();
+            path = path.substring(len, 1+len) + ":" + 
+                    path.substring(1+len);
+        }
+        if ((endLine == 0) && (line != 0)) {
+            endLine = line;
+        }
+        // XXX removed message/comment
+        return new SourceLocation(new File(path), line, endLine, column);
+    }
+
+    // XXX reconsider convert if used in production code
+    /**
+     * Convert String to int using ascii and optionally
+     * tolerating text
+     * @param s the String to convert
+     * @param permitText if true, pick a sequence of numbers 
+     *         within a possibly non-numeric String
+     * @param last if permitText, then if this is true the
+     *         last sequence is used - otherwise the first is used
+     * XXX only default u.s. encodings..
+     * @return -1 or value if a valid, totally-numeric positive string 0..MAX_WIDTH 
+     */
+    private static int convert(String s) {
+        return convert(s, false, false);
+    }
+    
+       // XXX reconsider convert if used in production code
+    /**
+     * Convert String to int using ascii and optionally
+     * tolerating text
+     * @param s the String to convert
+     * @param permitText if true, pick a sequence of numbers 
+     *         within a possibly non-numeric String
+     * @param last if permitText, then if this is true the
+     *         last sequence is used - otherwise the first is used
+     * XXX only default u.s. encodings..
+     * @return -1 or value if a valid, positive string 0..MAX_WIDTH 
+     */
+    private static int convert(String s, boolean permitText, 
+        boolean first) { 
+        int result = -1;
+        int last = -1;
+        int max = s.length(); 
+        boolean reading = false;
+        for (int i = 0; i < max; i++) {
+            char c = s.charAt(i);
+            if ((c >= '0') && (c <= '9')) {
+                if (-1 == result) { // prefix loop
+                    result = 0;
+                    reading = true;
+                }
+                result = ((result * 10) + (c - '0'));
+            } else if (!permitText) {
+                return -1;
+            } else if (reading) { // from numeric -> non-numeric
+                if (first) {
+                    return result;
+                } else {
+                    last = result;
+                }
+                reading = false;
+            }
+        }
+        if (permitText && !first && (-1 != last) && (-1 == result)) {
+            result = last;
+        }
+        return ((0 < result) && (result < ISourceLocation.MAX_LINE) ? result : -1);
+    }
+
+    private BridgeUtil() {}
+
+    /** @return String for status header, counting children passed/failed */
+    public static String childString(IRunStatus runStatus, int numSkips, int numIncomplete) {
+        if (null == runStatus) {
+            return "((RunStatus) null)";
+        }
+        if (0 > numSkips) {
+            numSkips = 0;
+        }
+        if (0 > numIncomplete) {
+            numIncomplete = 0;
+        }
+        StringBuffer sb = new StringBuffer();
+        if (RunValidator.NORMAL.runPassed(runStatus)) {
+            sb.append("PASS ");
+        } else {
+            sb.append("FAIL ");
+        }
+        Object id = runStatus.getIdentifier();
+        if (null != id) {
+            sb.append(id.toString() + " ");
+        }
+        IRunStatus[] children = runStatus.getChildren();
+        final int numChildren = (null == children ? 0 : children.length);
+        final int numTests = numIncomplete + numChildren + numSkips;
+        int numFails = 0;
+        if (!LangUtil.isEmpty(children)) {
+            for (int i = 0; i < children.length; i++) {
+                if (!RunValidator.NORMAL.runPassed(children[i])) {
+                    numFails++;
+                }
+                       }
+        }
+        final int numPass = children.length - numFails;
+        sb.append(numTests + " tests");
+        if (0 < numTests) {
+            sb.append(" (");
+        }
+        if (0 < numSkips) {
+            sb.append(numSkips + " skipped");
+            if (0 < (numFails + numPass + numIncomplete)) {
+                sb.append(", ");
+            }
+        }
+        if (0 < numIncomplete) {
+            sb.append(numIncomplete + " incomplete");
+            if (0 < (numFails + numPass)) {
+                sb.append(", ");
+            }
+        }
+        if (0 < numFails) {
+            sb.append(numFails + " failed");
+            if (0 < numPass) {
+                sb.append(", ");
+            }
+        }
+        if (0 < numPass) {
+            sb.append(numPass + " passed)");
+        } else if (0 < numTests) {
+            sb.append(")");
+        }
+        return sb.toString().trim();
+    }
+
+       /** @return String for status header */
+       public static String toShortString(IRunStatus runStatus) {
+        if (null == runStatus) {
+            return "((RunStatus) null)";
+        }
+        StringBuffer sb = new StringBuffer();
+        if (RunValidator.NORMAL.runPassed(runStatus)) {
+            sb.append("PASS ");
+        } else {
+            sb.append("FAIL ");
+        }
+        Object id = runStatus.getIdentifier();
+        if (null != id) {
+            sb.append(id.toString() + " ");
+        }
+        sb.append(MessageUtil.renderCounts(runStatus));
+        return sb.toString().trim();
+       }
+
+}
diff --git a/testing/src/org/aspectj/testing/util/CollectorFileFilter.java b/testing/src/org/aspectj/testing/util/CollectorFileFilter.java
new file mode 100644 (file)
index 0000000..ef268bc
--- /dev/null
@@ -0,0 +1,88 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** 
+ * Wrap FileFilter to collect any accepted 
+ */
+public class CollectorFileFilter implements FileFilter {
+    /** returned from getFiles() when there are no files to get */
+    public static final List EMPTY 
+        = Collections.unmodifiableList(new ArrayList(0));
+
+    /** used for collecting filters */
+    protected ArrayList files;
+
+    /** filter delegate - may be null */
+    protected final FileFilter filter;
+
+    /** return false from accept only when !alwaysTrue 
+     * and filter is null or fails 
+     */
+    protected final boolean alwaysTrue;
+
+    /** this(null, true) */
+    public CollectorFileFilter() {
+        this(null, true);
+    }
+
+    /*
+     * @param filter the FileFilter delegate - may be null
+    * @param alwaysTrue return false from accept only when !alwaysTrue 
+     * and filter is null or fails 
+    */
+    public CollectorFileFilter(FileFilter filter, boolean alwaysTrue){
+        this.filter = filter;
+        this.alwaysTrue = alwaysTrue;
+    }
+
+    /**
+     * Accept file into collection if filter is null or passes. 
+     * @return false only when !alwaysTrue and filter fails. 
+     */
+    public boolean accept(File f) {
+        if ((null == filter) || filter.accept(f)) {
+            add(f);
+            return true;
+        }
+        return alwaysTrue;
+    }
+
+    /** gather files */
+    protected synchronized void add(File f) {
+        if (null != f) {
+            if (null == files) {
+                files = new ArrayList();
+            }
+            files.add(f);
+        }
+    }
+
+    /** 
+     * return clone of gathered-files 
+     * @return EMPTY if no files or a clone of the collection otherwise
+     */
+    public synchronized List getFiles() {
+        if ((null == files) || (0 == files.size())) {
+            return EMPTY;
+        }
+        return (List) files.clone();
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/Diffs.java b/testing/src/org/aspectj/testing/util/Diffs.java
new file mode 100644 (file)
index 0000000..1adf409
--- /dev/null
@@ -0,0 +1,103 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.MessageUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+/** result struct for expected/actual diffs for Collection */
+public class Diffs {
+
+     // XXX List -> Collection b/c comparator orders 
+    public static final Diffs NONE 
+        = new Diffs("NONE", Collections.EMPTY_LIST, Collections.EMPTY_LIST);
+    
+    /** name of the thing being diffed - used only for reporting */
+    public final String label;
+    
+    /** immutable List */
+    public final List  missing; 
+
+    /** immutable List */
+    public final List unexpected;
+
+    /** true if there are any missing or unexpected */
+    public final boolean different;
+    
+    private Diffs(String label, List missing, List unexpected) {
+        this.label = label;
+        this.missing = missing;
+        this.unexpected = unexpected;
+        different = ((0 != this.missing.size()) 
+                      || (0 != this.unexpected.size()));
+    }
+
+    public Diffs(String label, List expected, List actual, Comparator comparator) {
+        label = label.trim();
+        if (null == label) {
+            label = ": ";
+        } else if (!label.endsWith(":")) {
+            label += ": ";
+        }
+        this.label = " " + label;
+        ArrayList miss = new ArrayList();
+        ArrayList unexpect = new ArrayList();
+        
+        org.aspectj.testing.util.LangUtil.makeSoftDiffs(expected, actual, miss, unexpect, comparator);
+        missing = Collections.unmodifiableList(miss);
+        unexpected = Collections.unmodifiableList(unexpect);
+        different = ((0 != this.missing.size()) 
+                      || (0 != this.unexpected.size()));
+    }
+    
+    /** 
+     * Report missing and extra items to handler.
+     * For each item in missing or unexpected, this creates a {kind} IMessage with 
+     * the text "{missing|unexpected} {label}: {message}"
+     * where {message} is the result of 
+     * <code>MessageUtil.renderMessage(IMessage)</code>.
+     * @param handler where the messages go - not null
+     * @param kind the kind of message to construct - not null
+     * @param label the prefix for the message text - if null, "" used
+     * @see MessageUtil#renderMessage(IMessage)
+     */
+    public void report(IMessageHandler handler, IMessage.Kind kind) {
+        LangUtil.throwIaxIfNull(handler, "handler");
+        LangUtil.throwIaxIfNull(kind, "kind");
+        if (different) {
+            for (Iterator iter = missing.iterator(); iter.hasNext();) {
+                String s = MessageUtil.renderMessage((IMessage) iter.next());
+                MessageUtil.fail(handler, "missing " + label + s);         
+            }
+            for (Iterator iter = unexpected.iterator(); iter.hasNext();) {
+                String s = MessageUtil.renderMessage((IMessage) iter.next());
+                MessageUtil.fail(handler, "unexpected " + label + s);         
+            }
+        }
+    }
+    
+    /** @return "{label}: (unexpected={#}, missing={#})" */
+    public String toString() {
+        return label + "(unexpected=" + unexpected.size() 
+            + ", missing=" + missing.size() + ")";
+    }
+}
+
diff --git a/testing/src/org/aspectj/testing/util/FileUtil.java b/testing/src/org/aspectj/testing/util/FileUtil.java
new file mode 100644 (file)
index 0000000..2caa5e2
--- /dev/null
@@ -0,0 +1,745 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringBufferInputStream;
+import java.io.StringWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Vector;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/** 
+ * misc file utilities
+ */
+public class FileUtil {
+
+    /** default filename if URL has none (i.e., a directory URL): index.html */
+    public static final String DEFAULT_URL_FILENAME = "index.html";
+
+    /**
+     * @param args the String[] 
+     *   <code>{ "-copy", "-srcFile" | "-srcUrl", {src}, "-destFile", {destFile} }</code>
+     */
+    public static void main (String[] args) {
+        if (null == args) return;
+        for (int i = 0; (i+4) < args.length; i++) {
+            if ("-copy".equals(args[i])) {
+                String arg = args[++i];
+                String src = null;
+                String destFile = null;
+                boolean srcIsFile = ("-srcFile".equals(arg));
+                if (srcIsFile) {
+                    src = args[++i];
+                } else if ("-srcUrl".equals(arg)) {
+                    src = args[++i];
+                }
+                if ((null != src) && ("-destFile".equals(args[++i]))) {
+                    destFile = args[++i];
+                    StringBuffer errs = new StringBuffer();
+                    if (srcIsFile) {
+                        copyFile(new File(src), new File(destFile), errs);
+                    } else {
+                        URL url = null;
+                        try { url = new URL(src) ; }
+                        catch (MalformedURLException e) { render(e, errs); }
+                        if (null != url) {
+                            copyURL(url, new File(destFile), errs);
+                        }
+                    }
+                    if (0 < errs.length()) {
+                        System.err.println("Error copying " + src + " to " + destFile);
+                        System.err.println(errs.toString());
+                            
+                    }
+                }
+            } // ("-copy".equals(args[i])){
+        } 
+    } // end of main ()
+    
+    /**
+     * Generate a list of missing and extra files by comparison to a 
+     * timestamp, optionally excluding certain files.
+     * This is a call to select all files after a given time:
+     * 
+     * <pre>Diffs d = dirDiffs(dir, givenTime, null, null, null);</pre> 
+     * 
+     * Given files
+     * <pre>classes/Foo.class
+     * classes/bar/Bash.class
+     * classes/Old.class
+     * classes/one/Unexpected.class
+     * classes/start.gif</pre>
+     * where only Old.class predated startTime, this is a call that 
+     * reports "one/Unexpected.class" as unexpected and "Foo"
+     * as missing:
+     * <pre>String requireSuffix = ".class";
+     * String[] expectedPaths = new String[] { "Foo", "bar/Bas" };
+     * File file = new File("classes");
+     * Diffs d = dirDiffs(dir, startTime, requireSuffix,expectedPaths, true);</pre> 
+     * 
+     * @param label the String to use for the Diffs label
+     * @param dir the File for the dir to search
+     * @param startTime collect files modified after this time
+     *         (ignored if less than 0)
+     * @param requireSuffix ignore all actual files without this suffix
+     *         (ignored if null)
+     * @param expectedPaths paths (relative to dir) of the expected files
+     *         (if null, none expected)
+     * @param acceptFilePrefix if true, then accept a file which
+     *         differs from an expected file name only by a suffix
+     *         (which need not begin with ".").
+     */
+    public static Diffs dirDiffs( // XXX too complicated, weak prefix checking
+        final String label,
+        final File dir, 
+        final long startTime, 
+        final String requiredSuffix, 
+        final String[] expectedPaths, 
+        final boolean acceptFilePrefix) {
+        
+        LangUtil.throwIaxIfNull(dir, "dir");
+        final boolean checkExpected = !LangUtil.isEmpty(expectedPaths);
+        
+        // normalize sources to ignore
+        final ArrayList expected = (!checkExpected ? null : new ArrayList());
+        if (checkExpected) {
+            for (int i = 0; i < expectedPaths.length; i++) {
+                String srcPath = expectedPaths[i];
+                if (!LangUtil.isEmpty(srcPath)) {
+                    expected.add(org.aspectj.util.FileUtil.weakNormalize(srcPath));
+                }
+            }
+        }
+        
+        // gather, normalize paths changed
+        FileFilter touchedCollector = new FileFilter() {
+            /** 
+             * For files complying with time and suffix rules,
+             * return true (accumulate - unexpected) 
+             * unless they match expected files,
+             * (deleting any matches from sources
+             * so the remainder is missing).
+             * @return true for unexpected files after date */
+            public boolean accept(File file) {
+                if (file.isFile()
+                    && ((0 > startTime) 
+                        || (startTime < file.lastModified()))) {
+                    String path = file.getPath();
+                    if ((null == requiredSuffix) || path.endsWith(requiredSuffix)) {                        
+                        path = org.aspectj.util.FileUtil.weakNormalize(path);
+                        if (checkExpected) {
+                            if (!acceptFilePrefix) {
+                                // File.equals(..) does lexical compare
+                                if (expected.contains(path)) {
+                                    expected.remove(path);
+                                    // found - do not add to unexpected
+                                    return false; 
+                                }                      
+                            } else {
+                                for (Iterator iter = expected.iterator();
+                                                                       iter.hasNext();
+                                                                       ) {
+                                                                       String exp = (String) iter.next();
+                                                                       if (path.startsWith(exp)) {                                        
+                                        String suffix = path.substring(exp.length());
+                                        if (-1 == suffix.indexOf("/")) { // normalized...
+                                            expected.remove(path);
+                                            // found - do not add to unexpected
+                                            return false; 
+                                        }
+                                    }
+                                                               }
+                            }
+                        }
+                        // add if is file, right time, and have or don't need suffix
+                        return true;
+                    }
+                }
+                // skip if not file or not right time
+                return false;
+            }
+        };      
+        ArrayList unexp = new ArrayList();
+        unexp.addAll(Arrays.asList(dir.listFiles(touchedCollector)));
+        
+        // report any unexpected changes
+        return new Diffs(label, expected, unexp, String.CASE_INSENSITIVE_ORDER);
+    }
+
+
+    /**
+     * Visit the entries in a zip file, halting when visitor balks.
+     * Errors are silently ignored.
+     * @throws IllegalArgumentException if zipfile or visitor is null
+     */
+    public static void visitZipEntries(ZipFile zipfile, StringVisitor visitor) {
+        visitZipEntries(zipfile, visitor, (StringBuffer) null);
+    }
+        
+    /**
+     * Visit the entries in a zip file, halting when visitor balks.
+     * Errors are reported in errs, if not null.
+     * @throws IllegalArgumentException if zipfile or visitor is null
+     */
+    public static void visitZipEntries(ZipFile zipfile, StringVisitor visitor, 
+                                       StringBuffer errs) {
+        if (null == zipfile) throw new IllegalArgumentException("null zipfile");
+        if (null == visitor) throw new IllegalArgumentException("null visitor");
+        int index = 0;
+        try {
+            Enumeration enum = zipfile.entries();
+            while (enum.hasMoreElements()) {
+                ZipEntry entry = (ZipEntry) enum.nextElement();
+                index++;
+                if (! visitor.accept(entry.getName())) {
+                    break;
+                }
+            }
+        } catch (Throwable e) {  
+            if (null != errs) {
+                errs.append("FileUtil.visitZipEntries error accessing entry " + index 
+                         + ": " + e.getMessage());
+                StringWriter sw = new StringWriter();
+                e.printStackTrace(new PrintWriter(sw));
+                errs.append(sw.toString());
+            }
+        } finally {
+            if (null != zipfile) {
+                try { zipfile.close(); } 
+                catch (IOException x) {} // ignore
+            }
+        }
+    }
+
+    /**
+        * descend filesystem tree, invoking FileFilter.accept() on files.
+        * E.g., To list files from current directory:
+        * <code><pre>descendFileTree(new File("."), new FileFilter() {
+        *     public boolean accept(File f){
+        *        System.out.println(f.getAbsolutePath());
+     *        return true;
+        *     }});</code></pre>
+        * @param file root/starting point.  If a file, the only one visited.
+        * @param filter supplies accept(File) routine
+        */
+    public static void descendFileTree(File file, FileFilter filter) {
+        descendFileTree(file, filter, false);
+    }
+
+    /**
+     * Descend filesystem tree, invoking FileFilter.accept() on files
+     * and, if userRecursion, on dirs. If userRecursion, accept() must
+     * call descendFileTree() again to recurse down directories.
+     * This calls fileFilter.accept(File) on all files before doing any dirs.
+     * E.g., To list only files from Unix root:
+     * <code><pre>descendFileTree(new File("/"), new FileFilter() {
+     *     public boolean run(File f){
+     *        System.out.println(f.getAbsolutePath());
+     *        return true;
+     *     }}, false);</code></pre>
+     * To list files/dir from root using user recursion:
+     * <code><pre>descendFileTree(new File("/"), new FileFilter() {
+     *     public boolean run(File f){ 
+     *        System.out.println(f.getAbsolutePath());
+     *        if (f.isDirectory() && (-1 == f.getName().indexOf("CVS")))
+     *           return descendFileTree(f, this, true);
+     *        return true;
+     *     }}, true);</code></pre>
+     * @param file root/starting point.  If a file, the only one visited.
+     * @param filter supplies boolean accept(File) method
+     * @param userRecursion - if true, do accept() on dirs; else, recurse
+     * @return false if any fileFilter.accept(File) did.
+     * @throws IllegalArgumentException if file or fileFilter is null
+     */
+    public static boolean descendFileTree(File file, FileFilter fileFilter, 
+                                          boolean userRecursion) {
+        if (null == file) {throw new IllegalArgumentException("parm File"); }
+        if (null == fileFilter){throw new IllegalArgumentException("parm FileFilter");}
+
+        if (!file.isDirectory()) {
+            return fileFilter.accept(file);
+        } else if (file.canRead()) { 
+            // go through files first
+            File[] files = file.listFiles(ValidFileFilter.FILE_EXISTS); 
+            if (null != files) {
+                for (int i = 0; i < files.length; i++) {
+                    if (!fileFilter.accept(files[i])) {
+                        return false;
+                    }
+                }
+            }
+            // now recurse to handle directories
+            File[] dirs = file.listFiles(ValidFileFilter.DIR_EXISTS);
+            if (null != dirs) {
+                for (int i = 0; i < dirs.length; i++) {
+                    if (userRecursion) {
+                        if (!fileFilter.accept(dirs[i])) {
+                            return false;
+                        }
+                    } else {
+                        if (!descendFileTree(dirs[i], fileFilter,userRecursion)) {
+                            return false;
+                        }
+                    }
+                }
+            }
+        } // readable directory (ignore unreadable ones) 
+        return true;
+    } // descendFiles
+
+    /**
+     * Return the names of all files below a directory.
+     * If file is a directory, then all files under the directory
+     * are returned.  If file is absolute or relative, all the files are.
+     * If file is a zip or jar file, then all entries in the zip or jar
+     * are listed.  Entries inside those jarfiles/zipfiles are not listed.
+     * There are no guarantees about ordering.
+     * @param dir the File to list for
+     * @param results the Collection to use for the results (may be null)
+     * @throws IllegalArgumentException if null == dir 
+     * @return a Collection of String of paths, including paths inside jars
+     */
+    public static Collection directoryToString(File dir, Collection results) {
+        if (null == dir) throw new IllegalArgumentException("null dir");
+        final Collection result = (results != null? results : new Vector());
+        if (isZipFile(dir)) {
+            zipFileToString(dir, result);
+        } else if (!dir.isDirectory()) {
+            throw new IllegalArgumentException("not a dir: " + dir);
+        } else {
+            AccumulatingFileFilter acFilter = new AccumulatingFileFilter() {
+                    public boolean accumulate(File file) {
+                        String name = file.getPath();
+                        result.add(name);
+                        if (isZipFile(file)) {
+                            zipFileToString(file, result);
+                        }
+                        return true;
+                    }
+                };
+            descendFileTree(dir, acFilter, false);
+        }
+        return result;
+    } // directoryToString
+
+    /**
+     * Render as String the entries in a zip or jar file,
+     * converting each to String beforehand (as jarpath!jarentry)
+     * applying policies for whitespace, etc.
+     * @param file the File to enumerate ZipEntry for
+     * @param results the Colection to use to return the FileLine - may be null
+     * @return FileLines with string as text and 
+     *        canonical as string modified by any canonicalizing policies.
+     */
+    public static Collection zipFileToString(final File zipfile, Collection results) {
+        Collection result = (results != null ? results : new Vector());
+        ZipFile zip = null;
+        try {
+            zip = new ZipFile(zipfile); // ZipFile.OPEN_READ| ZipFile.OPEN_DELETE); delete is 1.3 only
+            Enumeration enum = zip.entries();
+            while (enum.hasMoreElements()) {
+                results.add(renderZipEntry(zipfile, (ZipEntry) enum.nextElement()));
+            }
+            zip.close();
+            zip = null;
+        } catch (Throwable t) {
+            String err = "Error opening " + zipfile + " attempting to continue...";
+            System.err.println(err);
+            t.printStackTrace(System.err);
+        } finally {
+            if (null != zip) {
+                try { zip.close(); }
+                catch (IOException e) {
+                    e.printStackTrace(System.err);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @return true if file represents an existing file with a zip extension
+     */
+    public static boolean isZipFile(File f) {
+        String s = null;
+        if ((null == f)  || (null == (s = f.getPath()))) {
+            return false;
+        } else {
+            return (f.canRead() && !f.isDirectory() 
+                    && (s.endsWith(".zip")
+                        || (s.endsWith(".jar"))));
+        }
+    }
+
+    /**
+     * Render a zip/entry combination to String
+     */
+    public static String renderZipEntry(File zipfile, ZipEntry entry) {
+        String filename = (null == zipfile ? "null File" : zipfile.getName());
+        String entryname = (null == entry ? "null ZipEntry" : entry.getName());
+        return filename + "!" + entryname;
+    }
+
+    /**
+     * Write all files in directory out to jarFile
+     * @param jarFile the File to create and write to 
+     * @param directory the File representing the directory to read
+     * @param mainClass the value of the main class attribute - may be null
+     */
+    public static boolean createJarFile(File jarFile, File directory,
+                                        String mainClass, FileFilter filter) {
+        String label = "createJarFile("+jarFile
+            +","+directory +","+mainClass +","+filter + "): ";
+        Log.signal(label + " start");
+        if (null == directory) 
+            throw new IllegalArgumentException("null directory");
+        Manifest manifest = createManifest(mainClass);
+        Log.signal(label + " manifest=" + manifest);
+        JarOutputStream out =  null;
+        try {
+            File jarFileDir = jarFile.getParentFile();
+            if (null == jarFileDir) {
+                Log.signal(label + " null jarFileDir");
+            } else if (!jarFileDir.exists() && !jarFileDir.mkdirs()) { // XXX convert to Error
+                Log.signal(label + " unable to create jarFileDir: " + jarFileDir); 
+            }
+            OutputStream os = new FileOutputStream(jarFile);
+            out = (null == manifest ? new JarOutputStream(os)
+                : new JarOutputStream(os, manifest));
+            Log.signal(label + " out=" + out);
+            ZipAccumulator reader = new ZipAccumulator(directory, out, filter);
+            Log.signal(label + " reader=" + reader);
+            FileUtil.descendFileTree(directory, reader);
+            out.closeEntry();
+            return true;
+        } catch (IOException e) {
+            e.printStackTrace(System.err); // todo
+        } finally {
+            if (null != out) {
+                try { out.close();}
+                catch (IOException e) {} // todo ignored
+            }
+        }
+                                   
+        return false;
+    }    
+
+    protected static Manifest createManifest(String mainClass) {
+        final String mainKey = "Main-Class";
+        Manifest result = null;
+        if (null != mainClass) {
+            String entry = "Manifest-Version: 1.0\n"
+                + mainKey + ": " + mainClass + "\n";
+            try {
+                result = new Manifest(new StringBufferInputStream(entry));
+                Attributes attributes = result.getMainAttributes();
+                String main = attributes.getValue(mainKey);
+                if (null == main) {
+                    attributes.putValue(mainKey, mainClass);
+                    main = attributes.getValue(mainKey);
+                    if (null == main) {
+                        Log.signal("createManifest unable to set main " 
+                                   + mainClass);
+                    }
+                }
+            } catch (IOException e) { // todo ignoring
+                Log.signal(e, " IOException creating manifest with " + mainClass);
+            }
+        }
+        return result;
+    }
+
+
+    /** read a file out to the zip stream */
+    protected static void addFileToZip(File in, File parent,
+                                       ZipOutputStream out) 
+        throws IOException {
+        String path = in.getCanonicalPath();
+        String parentPath = parent.getCanonicalPath();
+        if (!path.startsWith(parentPath)) {
+            throw new Error("not parent: " + parentPath + " of " + path);
+        } else {
+            path = path.substring(1+parentPath.length());
+            path = path.replace('\\', '/'); // todo: use filesep
+        }
+        ZipEntry entry = new ZipEntry(path);
+        entry.setTime(in.lastModified());
+        // todo: default behavior is DEFLATED
+
+        out.putNextEntry(entry);
+
+        InputStream input = null;
+        try {
+            input = new FileInputStream(in);
+            byte[] buf = new byte[1024];
+            int count;
+            while (0 < (count = input.read(buf, 0, buf.length))) {
+                out.write(buf, 0, count);
+            }
+        } finally {
+            if (null != input) input.close(); 
+        }
+    }    
+
+
+    public static void returnTempDir(File dir) {
+        deleteDirectory(dir);
+    }
+
+    /** @return true if path ends with gif, properties, jpg */
+    public static boolean isResourcePath(String path) {
+        if (null == path) return false;
+        path = path.toLowerCase();
+        return (path.endsWith(".gif")
+                || path.endsWith(".properties")
+                || path.endsWith(".jpg")
+                || path.endsWith(".jpeg")
+                || path.endsWith(".props")
+                );
+    }
+
+    public static void render(Throwable t, StringBuffer err) { // todo: move
+        String name = t.getClass().getName();
+        int loc = name.lastIndexOf(".");
+        name = name.substring(1+loc);
+        err.append(name + ": " + t.getMessage() + "\n"); // todo
+        StringWriter sw = new StringWriter();
+        t.printStackTrace(new PrintWriter(sw));
+        err.append(sw.toString());
+    }
+
+    private static boolean report(StringBuffer err, String context, String status, 
+                                  Throwable throwable) {
+        boolean failed = ((null != status) || (null != throwable));
+        if ((null != err) && (failed)) {
+            if (null != context) {
+                err.append(context);
+            }
+            if (null != status) {
+                err.append(status);
+            }
+            if (null != throwable) {
+                render(throwable, err);
+            }
+        }
+        return failed;
+    }
+
+    /** 
+     * Copy file.
+     * @param src the File to copy - must exist
+     * @param dest the File for the target file or directory (will not create directories)
+     * @param err the StringBuffer for returning any errors - may be null
+     **/
+    public static boolean copyFile(File src, File dest, StringBuffer err) {
+        boolean result = false;
+        String label = "start";
+        Throwable throwable = null;
+        try {
+            if (!ValidFileFilter.FILE_EXISTS.accept(src)) {
+                label = "src file does not exist";
+            } else {
+                if (dest.isDirectory()) {
+                    dest = new File(dest, src.getName());
+                }
+                if (ValidFileFilter.FILE_EXISTS.accept(dest)) {
+                    label = "dest file exists";
+                } 
+                boolean closeWhenDone = true;
+                result = copy(new FileInputStream(src),
+                              new FileOutputStream(dest),
+                              closeWhenDone);
+            }
+            label = null;
+        } catch (Throwable t) {
+            throwable = t;
+        }
+        String context = "FileUtil.copyFile(src, dest, err)"; 
+        boolean report = report(err, context, label, throwable); 
+        return (result && report);
+    }
+
+    /** 
+     * Copy URL to file.
+     * @param src the URL to copy - must exist
+     * @param dest the File for the target file or directory (will not create directories)
+     * @param err the StringBuffer for returning any errors - may be null
+     **/
+    public static boolean copyURL(URL url, File dest, StringBuffer err) { // todo untested.
+        boolean result = false;
+        String label = "start";
+        Throwable throwable = null;
+        try {
+            if (dest.isDirectory()) {
+                String filename = url.getFile();
+                if ((null == filename) || (0 == filename.length())) {
+                    filename = DEFAULT_URL_FILENAME;
+                }
+                dest = new File(dest, filename);
+            }
+            if (ValidFileFilter.FILE_EXISTS.accept(dest)) {
+                label = "dest file exists";
+            } 
+            boolean closeWhenDone = true;
+            result = copy(url.openConnection().getInputStream(),
+                          new FileOutputStream(dest),
+                          closeWhenDone);
+            label = null;
+        } catch (Throwable t) {
+            throwable = t;
+        }
+        String context = "FileUtil.copyURL(src, dest, err)"; // add actual parm to labels?
+        boolean report = report(err, context, label, throwable); 
+        return (result && report);
+    }
+
+     /** 
+     * Copy input to output - does not close either
+     * @param src the InputStream to copy - must exist
+     * @param dest the OutputStream for the target 
+     * @param close if true, close when done
+     */
+    public static boolean copy(InputStream src, OutputStream dest, 
+                               boolean close) 
+        throws IOException {
+        boolean result = false;
+        IOException throwable = null;
+        try {
+            byte[] buf = new byte[8*1024];
+            int count;
+            while (0 < (count = src.read(buf, 0, buf.length))) {
+                dest.write(buf, 0, count);
+            }
+            result = true;
+        } catch (IOException t) {
+            throwable = t;
+        } finally {
+            if (close) {
+                try { if (null != src) src.close(); } 
+                catch (IOException e) {
+                    if (null == throwable) { throwable = e; }
+                }
+                try { if (null != dest) dest.close(); } 
+                catch (IOException i) {
+                    if (null == throwable) { throwable = i; }
+                }
+            }
+        }
+        if (null != throwable) throw throwable;
+        return result;
+    }
+
+    /**
+     * @return true if dir was an existing directory that is now deleted
+     */
+    protected static boolean deleteDirectory(File dir) {
+        return ((null != dir) 
+                && dir.exists()
+                && dir.isDirectory()
+                && FileUtil.descendFileTree(dir, DELETE_FILES, false)
+                && FileUtil.descendFileTree(dir, DELETE_DIRS, true)
+                && dir.delete());
+    }
+
+       public static String[] getPaths(File[] files) { // util
+        String[] result = new String[files.length];
+        for (int i = 0; i < result.length; i++) {
+                       result[i] = files[i].getPath(); // preserves absolute?
+               }
+        return result;
+    }
+
+    //-------- first-order, input and visible interface  
+
+    protected static final FileFilter DELETE_DIRS = new FileFilter() {
+            public boolean accept(File file) {
+                return ((null != file) && file.isDirectory()
+                        && file.exists() && file.delete());
+            }
+        };
+    protected static final FileFilter DELETE_FILES = new FileFilter() {
+            public boolean accept(File file) {
+                return ((null != file) && !file.isDirectory()
+                        && file.exists() && file.delete());
+            }
+        };
+
+} // class FileUtil
+
+/** 
+ * Localize FileUtil log/signals for now 
+ * ordinary signals are ignored,
+ * but exceptions are printed to err
+ * and errors are thrown as Error
+  */
+class Log {
+    /** ordinary logging - may be suppressed */
+    public static final void signal(String s) {
+        //System.err.println(s);
+    }
+    /** print stack trace to System.err */
+    public static final void signal(Throwable t, String s) {
+        System.err.println(s);
+        t.printStackTrace(System.err);
+    }
+    /** @throws Error(s) always */
+    public static final void error(String s) {
+       throw new Error(s);
+    }
+}
+
+/** read each file out to the zip file */
+class ZipAccumulator implements FileFilter {
+    final File parentDir;
+    final ZipOutputStream out;
+    final FileFilter filter;
+    public ZipAccumulator(File parentDir, ZipOutputStream out,
+                          FileFilter filter) {
+        this.parentDir = parentDir;
+        this.out = out;
+        this.filter = filter;
+    }
+    public boolean accept(File f) {
+        if ((null != filter) && (!filter.accept(f))) {
+            return false;
+        }
+        try {
+            FileUtil.addFileToZip(f, parentDir, out);
+            return true;
+        } catch (IOException e) {
+            e.printStackTrace(System.err); // todo
+        }
+        return false;
+    }
+}
+
diff --git a/testing/src/org/aspectj/testing/util/IntRange.java b/testing/src/org/aspectj/testing/util/IntRange.java
new file mode 100644 (file)
index 0000000..80d547e
--- /dev/null
@@ -0,0 +1,118 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+
+import java.io.Serializable;
+
+/**
+ * imutable class to enforce an integer range
+ */
+public class IntRange implements IntValidator, ObjectChecker, Serializable {
+    /** no values permitted */
+    public static final IntRange NONE = new IntRange(0, 0);
+    /** 0 permitted */
+    public static final IntRange ZERO = new IntRange(0, 1);
+    /** 1 permitted */
+    public static final IntRange ONE = new IntRange(1, 2);
+    /** 0..1 permitted */
+    public static final IntRange OPTIONAL = new IntRange(0, 2);
+    /** 1..1000 permitted */
+    public static final IntRange MANY = new IntRange(1, 1001);
+
+    /** all positive numbers permitted except Integer.MAX_VALUE */
+    public static final IntRange POSITIVE = new IntRange(1, Integer.MAX_VALUE);
+    /** all negative numbers permitted  */
+    public static final IntRange NEGATIVE = new IntRange(Integer.MIN_VALUE, 0);
+    /** any int number permitted except Integer.MAX_VALUE */
+    public static final IntRange ANY = new IntRange(Integer.MIN_VALUE, Integer.MAX_VALUE);
+
+    /** 
+     * Make an IntRange that accepts this value
+     * (using existing if available).
+     * @throws IllegalArgumentException if value is Integer.MAX_VALUE.
+     */
+    public static final IntRange make(int value) {
+        switch (value) {
+            case (1) : return ONE;   
+            case (0) : return ZERO;  
+            case (Integer.MAX_VALUE) : 
+                throw new IllegalArgumentException("illegal " + value); 
+            default : 
+                return new IntRange(value, value + 1);
+        }
+    }
+      
+    public final int min;
+    public final int max;
+    private transient String cache;
+
+    /** use only for serialization 
+     * @deprecated IntRange(int, int)
+     */
+    protected IntRange() {
+        min = 0;
+        max = 0;
+    }    
+    
+    /**
+     * @param min minimum permitted value, inclusive
+     * @param max maximum permitted value, exclusive
+     */
+    public IntRange(int min, int max) {
+        this.min = min;
+        this.max = max;
+        if (min > max) {
+            throw new IllegalArgumentException( min + " > " + max);
+        }
+        toString(); // create cache to view during debugging
+    }
+    
+    /** @return true if integer instanceof Integer with acceptable intValue */
+    public final boolean isValid(Object integer) {
+        return ((integer instanceof Integer)
+                && (acceptInt(((Integer) integer).intValue())));
+    }
+    
+    /** @return true if min <= value < max */
+    public final boolean acceptInt(int value) {
+        return ((value >= min) && (value < max));
+    }
+    
+
+    /** 
+     * @deprecated acceptInt(int)
+     * @return true if min <= value < max 
+     */
+    public final boolean inRange(int value) {
+        return acceptInt(value);
+    }
+    /** 
+     * @return true if, for any int x s.t. other.inRange(x)
+     * is true, this.inRange(x) is also true
+     */
+    public final boolean inRange(IntRange other) {
+        return ((null != other)  && (other.min >= min)
+            && (other.max <= max));
+    }
+    
+    // XXX equals(Object)    
+    
+    public String toString() {
+        if (null == cache) {
+            cache = "IntRange [" + min + ".." + max + "]";
+        }
+        return cache;
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/IntValidator.java b/testing/src/org/aspectj/testing/util/IntValidator.java
new file mode 100644 (file)
index 0000000..ea3074d
--- /dev/null
@@ -0,0 +1,21 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+/**
+ * @author isberg
+ */
+public interface IntValidator {
+    /** @return true if this is a valid value */
+    public boolean acceptInt(int value);
+}
diff --git a/testing/src/org/aspectj/testing/util/IteratorWrapper.java b/testing/src/org/aspectj/testing/util/IteratorWrapper.java
new file mode 100644 (file)
index 0000000..266acb1
--- /dev/null
@@ -0,0 +1,147 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.util.Iterator;
+import java.util.List;
+
+/** 
+ * This iterates in order through the permutations of Lists. 
+ * Order and numericity depend on the underlying list iterators
+ * and the order in which the lists are supplied to the constructor.
+ * @author isberg
+ */
+public class IteratorWrapper implements Iterator {
+
+    final List[] lists;
+    final Iterator[] iterators;
+    final Object[] current;  
+    Object[] next;
+    
+    /** number of elements in each array */
+    final int maxDepth;
+
+    /** 
+     * Current level being iterated.
+     * Set to 0 whenever depth is incremented.
+     * Incremented when iterator for the level has no more elements.
+     */
+    int currentLevel; 
+    
+    /** 
+     * Maximum depth iterated-to thus far.
+     * Set to 0 on initialization.
+     * Incremented when incrementing currentLevel brings it past depth.
+     * Run completes when depth = maxDepth or any new iterator has no elements
+     */
+    int depth; 
+
+
+    /** @throws IllegalArgumentException if lists or any element null */
+       public IteratorWrapper(List[] lists) {
+        if (null == lists) {
+            throw new IllegalArgumentException("null lists");
+        }
+        maxDepth = lists.length;
+        currentLevel = 0;
+        depth = 0;
+        List[] temp = new List[maxDepth];
+        System.arraycopy(lists, 0, temp, 0, temp.length);
+        for (int i = 0; i < maxDepth; i++) {
+            if (null == temp[i]) {
+                throw new IllegalArgumentException("null List[" + i + "]");
+            }
+               }
+        this.lists = temp;
+        current = new Object[maxDepth];
+        iterators = new Iterator[maxDepth];
+        reset();
+    }
+    
+    /** Reset to the initial state of the iterator */
+    public void reset() {
+        next = null;
+        for (int i = 0; i < lists.length; i++) {
+                       iterators[i] = lists[i].iterator();
+            if (!iterators[i].hasNext()) { // one iterator is empty - never go
+                depth = maxDepth;
+                break;
+            } else {
+                current[i] = iterators[i].next();
+            }
+               }
+        if (depth < maxDepth) {
+            next = getCurrent();
+        }
+       }
+
+    /** @throws UnsupportedOperationException always */
+    public void remove() {
+        throw new UnsupportedOperationException("operation ambiguous");
+    }
+
+       public boolean hasNext() {
+               return (null != next);
+       }
+
+    /** 
+     * @return Object[] with each element from the iterator of the
+     * corresponding list which was passed to the constructor for this.
+     */
+       public Object next() {
+        Object result = next;
+        next = getNext();
+        return result;
+    }
+
+    private Object[] getCurrent() {
+        Object[] result = new Object[maxDepth];
+        System.arraycopy(current, 0, result, 0, maxDepth);
+        return result;
+    }
+    
+    private Object[] getNext() {
+        int initialLevel = currentLevel;
+        while (depth < maxDepth) {
+            if (iterators[currentLevel].hasNext()) {
+                current[currentLevel] = iterators[currentLevel].next();
+                if (currentLevel > initialLevel) {
+                    currentLevel = 0;
+                }
+                return getCurrent();
+            } else { // pop 
+                // reset this level
+                iterators[currentLevel] = lists[currentLevel].iterator();
+                if (!iterators[currentLevel].hasNext()) { // empty iterator - quit
+                    depth = maxDepth;
+                    return null;
+                }
+                current[currentLevel] = iterators[currentLevel].next();
+    
+                // do the next level
+                currentLevel++;
+                if (currentLevel > depth) {
+                    depth++;
+                }
+            }
+        }
+        return null;
+    }
+    /** @return "IteratorWrapper({{field}={value}}..)" for current, ceiling, and max */
+    public String toString() {
+        return "IteratorWrapper(currentLevel=" + currentLevel
+            + ", maxLevel=" + depth
+            + ", size=" + maxDepth + ")";
+    }    
+}
diff --git a/testing/src/org/aspectj/testing/util/LangUtil.java b/testing/src/org/aspectj/testing/util/LangUtil.java
new file mode 100644 (file)
index 0000000..472d35a
--- /dev/null
@@ -0,0 +1,1192 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.IMessage;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+
+/** 
+ * misc lang utilities
+ */
+public class LangUtil {
+
+    /** Delimiter used by split(String) (and ArrayList.toString()?) */
+    public static final String SPLIT_DELIM = ", ";
+
+    /** prefix used by split(String) (and ArrayList.toString()?) */
+    public static final String SPLIT_START = "[";
+
+    /** suffix used by split(String) (and ArrayList.toString()?) */
+    public static final String SPLIT_END = "]";
+
+    /** system-dependent classpath separator */
+    public static final String CLASSPATH_SEP;
+
+    private static final String[] NONE = new String[0];
+    
+    /** bad: hard-wired unix, windows, mac path separators */
+    private static final char[] SEPS = new char[] { '/', '\\', ':' };
+    
+    static {
+        // XXX this has to be the wrong way to get system-dependent classpath separator
+        String ps = ";";
+        try {
+            ps = System.getProperty("path.separator");
+            if (null == ps) {
+                ps = ";";
+                String cp = System.getProperty("java.class.path");
+                if (null != cp) {
+                    if (-1 != cp.indexOf(";")) {
+                        ps = ";";
+                    } else if (-1 != cp.indexOf(":")) {
+                        ps = ":";
+                    }
+                    // else warn?
+                }
+            }
+        } catch (Throwable t) { // ignore
+        } finally {
+            CLASSPATH_SEP = ps;
+        }
+    }  
+    
+       /**
+     * @return input if any are empty or no target in input,
+     *           or input with escape prefixing all original target
+     */
+    public static String escape(String input, String target, String escape) {
+        if (isEmpty(input) || isEmpty(target) || isEmpty(escape)) {
+            return input;
+        }
+        StringBuffer sink = new StringBuffer();
+        escape(input, target, escape, sink);
+        return sink.toString();
+    }
+    
+       /** 
+     * Append escaped input to sink.
+     * Cheap form of arbitrary escaping does not escape the escape String
+     * itself, but unflatten treats it as significant only before the target.
+     * (so this fails with input that ends with target).
+     */
+    public static void escape(String input, String target, String escape, StringBuffer sink) {
+        if ((null == sink) || isEmpty(input) || isEmpty(target) || isEmpty(escape)) {
+            return;
+        } else if (-1 == input.indexOf(target)) { // avoid StringTokenizer construction
+            sink.append(input);
+            return;
+        }
+        throw new Error("unimplemented");
+    }
+
+    /** flatten list per spec to sink */
+    public static void flatten(List list, FlattenSpec spec, StringBuffer sink) {
+        throwIaxIfNull(spec, "spec");
+        final FlattenSpec s = spec;
+        flatten(list, s.prefix, s.nullFlattened, s.escape, s.delim, s.suffix, sink);
+    }
+
+       /**
+     * Flatten a List to String by first converting to String[] 
+     * (using toString() if the elements are not already String)
+     * and calling flatten(String[]...).
+     */
+    public static void flatten(
+        List list, 
+        String prefix, 
+        String nullFlattened, 
+        String escape,
+        String delim, 
+        String suffix,
+        StringBuffer sink) {
+        throwIaxIfNull(list, "list");
+        Object[] ra = list.toArray();
+        String[] result;
+        if (String.class == ra.getClass().getComponentType()) {
+            result = (String[]) ra;
+        } else {
+            result = new String[ra.length];
+            for (int i = 0; i < result.length; i++) {
+                if (null != ra[i]) {
+                    result[i] = ra[i].toString();
+                }
+            }
+        }
+        flatten(result, prefix, nullFlattened, escape, delim, suffix, sink);
+    }
+
+
+
+
+       /** flatten String[] per spec to sink */
+    public static void flatten(String[] input, FlattenSpec spec, StringBuffer sink) {
+        throwIaxIfNull(spec, "spec");
+        final FlattenSpec s = spec;
+        flatten(input, s.prefix, s.nullFlattened, s.escape, s.delim,s.suffix, sink);
+    }
+    
+
+
+
+       /**
+     * Flatten a String[] to String by writing strings to sink,
+     * prefixing with leader (if not null),
+     * using nullRendering for null entries (skipped if null),
+     * escaping any delim in entry by prefixing with escape (if not null),
+     * separating entries with delim (if not null),
+     * and suffixing with trailer (if not null).
+     * Note that nullFlattened is not processed for internal delim,
+     * and strings is not copied before processing.
+     * @param strings the String[] input - not null
+     * @param prefix the output starts with this if not null
+     * @param nullFlattened the output of a null entry - entry is skipped (no delim) if null
+     * @param escape any delim in an item will be prefixed by escape if not null
+     * @param delim two items in the output will be separated by delim if not null
+     * @param suffix the output ends with this if not null
+     * @param sink the StringBuffer to use for output
+     * @return null if sink is not null (results added to sink) or rendering otherwise
+     */
+    public static void flatten(
+        String[] strings, 
+        String prefix, 
+        String nullFlattened, 
+        String escape,
+        String delim, 
+        String suffix,
+        StringBuffer sink) {
+        throwIaxIfNull(strings, "strings");
+        if (null == sink) {
+            return;
+        }
+        final boolean haveDelim = (!isEmpty(delim));
+        final boolean haveNullFlattened = (null != nullFlattened);
+        final boolean escaping = (haveDelim && (null != escape));
+        final int numStrings = (null == strings ? 0 : strings.length);
+        if (null != prefix) {
+            sink.append(prefix);
+        }
+        for (int i = 0; i < numStrings; i++) {
+                       String s = strings[i];
+            if (null == s) {
+                if (!haveNullFlattened) {
+                    continue;
+                }
+                if (haveDelim && (i > 0)) {
+                    sink.append(delim);
+                }
+                sink.append(nullFlattened);
+            } else {
+                if (haveDelim && (i > 0)) {
+                    sink.append(delim);
+                }
+                
+                if (escaping) {
+                    escape(s, delim, escape, sink);
+                } else {
+                    sink.append(s);
+                }
+            }
+               }
+        if (null != suffix) {
+            sink.append(suffix);
+        }
+    }
+       
+       /** @return ((null == ra) || (0 == ra.length)) */
+    public static boolean isEmpty(Object[] ra) {
+        return ((null == ra) || (0 == ra.length));
+    }
+
+       /** @return ((null == s) || (0 == s.length())); */
+    public static boolean isEmpty(String s) {
+        return ((null == s) || (0 == s.length()));
+    }
+
+       /**
+     * Shorthand for "if false, throw IllegalArgumentException"
+     * @throws IllegalArgumentException "{message}" if test is false
+     */
+    public static final void throwIaxIfFalse(final boolean test, final String message) {
+        if (!test) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+    
+       /**
+     * Shorthand for "if null, throw IllegalArgumentException"
+     * @throws IllegalArgumentException "null {name}" if o is null 
+     */
+    public static final void throwIaxIfNull(final Object o, final String name) {
+        if (null == o) {
+            String message = "null " + (null == name ? "input" : name);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+       public static ArrayList unflatten(String input, FlattenSpec spec) {
+        throwIaxIfNull(spec, "spec");
+        final FlattenSpec s = spec;
+        return unflatten(input,s.prefix, s.nullFlattened, s.escape, s.delim, s.suffix, s.emptyUnflattened);
+    }
+
+       /**
+     * Unflatten a String to String[] by separating elements at delim,
+     * handling prefixes, suffixes, escapes, etc.
+     * Any prefix or suffix is stripped from the input
+     * (or, if not found, an IllegalArgumentException is thrown).
+     * If delim is null or empty or input contains no delim, 
+     * then return new String[] {stripped input}.
+     * 
+     * XXX fix comments
+     * prefixing with leader (if not null),
+     * using nullRendering for null entries (skipped if null),
+     * escaping any delim in entry by prefixing with escape (if not null),
+     * separating entries with delim (if not null),
+     * and suffixing with trailer (if not null).
+     * Note that nullRendering is not processed for internal delim,
+     * and strings is not copied before processing.
+     * @param strings the String[] input - not null
+     * @param prefix the output starts with this if not null
+     * @param nullRendering the output of a null entry - entry is skipped (no delim) if null
+     * @param escape any delim in an item will be prefixed by escape if not null
+     * @param delim two items in the output will be separated by delim if not null
+     * @param suffix the output ends with this if not null
+     * @param sink the StringBuffer to use for output
+     * @return null if sink is not null (results added to sink) or rendering otherwise
+     * @throws IllegalArgumentException if input is null
+     *          or if any prefix does not start the input
+     *          or if any suffix does not end the input
+     */
+    public static ArrayList unflatten(
+        String input, 
+        String prefix, 
+        String nullFlattened, 
+        String escape,
+        String delim, 
+        String suffix,
+        String emptyUnflattened) {
+        throwIaxIfNull(input, "input");
+        final boolean haveDelim = (!isEmpty(delim));
+        final boolean haveNullFlattened = (null != nullFlattened);
+        final boolean escaping = (haveDelim && (null != escape));
+        if (!isEmpty(prefix)) {
+            if (input.startsWith(prefix)) {
+                input = input.substring(prefix.length());
+            } else {
+                String s = "expecting \"" + prefix + "\" at start of " + input + "\"";
+                throw new IllegalArgumentException(s);
+            }
+        }
+        if (!isEmpty(suffix)) {
+            if (input.endsWith(suffix)) {
+                input = input.substring(0, input.length() - suffix.length());
+            } else {
+                String s = "expecting \"" + suffix + "\" at end of " + input + "\"";
+                throw new IllegalArgumentException(s);
+            }
+        }
+
+        final ArrayList result = new ArrayList();
+        if (isEmpty(input)) {
+            return result;
+        }
+        if ((!haveDelim) || (-1 == input.indexOf(delim))) {
+            result.add(input);
+            return result;
+        }
+        
+        StringTokenizer st = new StringTokenizer(input, delim, true);
+        StringBuffer cur = new StringBuffer();
+        boolean lastEndedWithEscape = false;
+        boolean lastWasDelim = false;
+        while (st.hasMoreTokens()) {
+            String token = st.nextToken();
+            System.out.println("reading " + token);
+            if (delim.equals(token)) {
+            } else {
+                result.add(token);
+            }
+        }
+        return result;
+    }
+      
+    /** combine two string arrays, removing null and duplicates 
+     * @return concatenation of both arrays, less null in either or dups in two
+     * @see Util#combine(Object[], Object[])
+     */
+    public static String[] combine(String[] one, String[] two) {
+        ArrayList twoList = new ArrayList();
+        twoList.addAll(org.aspectj.util.LangUtil.arrayAsList(two));
+        ArrayList result = new ArrayList();
+        if (null != one) {
+            for (int i = 0; i < one.length; i++) {
+                if (null != one[i]) {
+                    twoList.remove(one[i]);
+                    result.add(one[i]);
+                }
+            }
+        }
+        for (Iterator iterator = twoList.iterator(); iterator.hasNext(); ) {
+                       String element = (String) iterator.next();
+                       if (null != element) {
+                result.add(element);
+            }
+               }
+        return (String[]) result.toArray(NONE);
+    }
+    
+    public static Properties combine(Properties dest, Properties add, boolean respectExisting)  { // XXX
+        if (null == add) return dest;
+        if (null == dest) return add;
+        for (Iterator iterator = add.keySet().iterator(); iterator.hasNext(); ) {
+            String key = (String) iterator.next();
+            if (null == key) {
+                continue;
+            }
+            String value = add.getProperty(key);
+            if (null == value) {
+                continue;
+            }
+            if (! respectExisting || (null == dest.getProperty(key))) {
+                dest.setProperty(key, value);           
+            }
+        }
+        return dest;
+    }
+
+    public static List arrayAsList(Object[] ra) {
+        return org.aspectj.util.LangUtil.arrayAsList(ra);
+    }
+    
+    /**
+     * return the fully-qualified class names
+     * inferred from the file names in dir
+     * assuming dir is the root of the source tree
+     * and class files end with ".class".
+     * @throws Error if dir is not properly named as prefix
+     *               of class files found in dir.
+     */
+    public static String[] classesIn(File dir) {
+        boolean alwaysTrue = true;
+        FileFilter filter = ValidFileFilter.CLASS_FILE;
+        CollectorFileFilter collector = new CollectorFileFilter(filter, alwaysTrue);
+        FileUtil.descendFileTree(dir, collector);
+        List list = collector.getFiles();
+        String[] result = new String[list.size()];
+        Iterator it = list.iterator();
+        String dirPrefix = dir.getPath();
+        for (int i = 0; i < result.length; i++) {
+            if (!it.hasNext()) {
+                throw new Error("unexpected end of list at " + i);
+            } 
+            result[i] = fileToClassname((File) it.next(), dirPrefix);
+        } 
+        return result;
+    }
+    
+    /**
+     * Convert String[] to String by using conventions for
+     * split.  Will ignore any entries containing SPLIT_DELIM
+     * (and write as such to errs if not null).
+     * @param input the String[] to convert
+     * @param errs the StringBuffer for error messages (if any)
+     */
+    public static String unsplit(String[] input, StringBuffer errs) {     
+        StringBuffer sb = new StringBuffer();
+        sb.append(SPLIT_START);
+        for (int i = 0; i < input.length; i++) {
+            if (-1 != input[i].indexOf(SPLIT_DELIM)) {
+                if (null != errs) {
+                    errs.append("\nLangUtil.unsplit(..) - item " + i + ": \"" + input[i]
+                        + " contains \"" + SPLIT_DELIM + "\"");
+                }
+            } else {
+                sb.append(input[i]);
+                if (1+i < input.length) {
+                    sb.append(SPLIT_DELIM);
+                }
+            }
+        }
+        sb.append(SPLIT_END);
+        return sb.toString();
+        
+    }
+    
+    /**
+     * Split input into substrings on the assumption that it is
+     * either only one string or it was generated using List.toString(),
+     * with tokens
+     * <pre>SPLIT_START {string} { SPLIT_DELIM {string}} SPLIT_END<pre>
+     * (e.g., <code>"[one, two, three]"</code>).
+     */
+    public static String[] split(String s) {
+        if (null == s) {
+            return null;
+        }
+        if ((!s.startsWith(SPLIT_START)) || (!s.endsWith(SPLIT_END))) {
+           return new String[] { s };
+        }
+        s = s.substring(SPLIT_START.length(),s.length()-SPLIT_END.length());
+        final int LEN = s.length();
+        int start = 0;
+        final ArrayList result = new ArrayList();
+        final String DELIM = ", ";
+        int loc = s.indexOf(SPLIT_DELIM, start);
+        while ((start < LEN) && (-1 != loc)) {
+            result.add(s.substring(start, loc));
+            start = DELIM.length() + loc;
+            loc = s.indexOf(SPLIT_DELIM, start);
+        }
+        result.add(s.substring(start));
+        return (String[]) result.toArray(new String[0]);
+    }
+    public static String[] strip(String[] src, String[] toStrip) {
+        if (null == toStrip) {
+            return strip(src, NONE);
+        } else if (null == src) {
+            return strip(NONE, toStrip);
+        }
+        List slist = org.aspectj.util.LangUtil.arrayAsList(src);
+        List tlist = org.aspectj.util.LangUtil.arrayAsList(toStrip);
+        slist.removeAll(tlist);
+        return (String[]) slist.toArray(NONE);        
+    }
+    
+    /**
+     * Load all classes specified by args, logging success to out
+     * and fail to err.
+     */    
+    public static void loadClasses(String[] args, StringBuffer out,
+                                   StringBuffer err) {
+        if (null != args) {
+            for (int i = 0; i < args.length; i++) {
+                try {
+                    Class c = Class.forName(args[i]);
+                    if (null != out) {
+                        out.append("\n");
+                        out.append(args[i]);
+                        out.append(": ");
+                        out.append(c.getName());
+                    }
+                } catch (Throwable t) {
+                    if (null != err) {
+                        err.append("\n");
+                        FileUtil.render(t, err);
+                    }
+                }
+            } 
+            
+        }
+    } 
+    
+    private static String fileToClassname(File f, String prefix) {
+        // this can safely assume file exists, starts at base, ends with .class
+        // this WILL FAIL if full path with drive letter on windows 
+        String path = f.getPath();
+        if (!path.startsWith(prefix)) {
+            String err = "!\"" + path + "\".startsWith(\"" + prefix + "\")";
+            throw new IllegalArgumentException(err);
+        }
+        int length = path.length() - ".class".length();
+        path = path.substring(prefix.length()+1, length);
+        for (int i = 0; i < SEPS.length; i++) {
+            path = path.replace(SEPS[i], '.');
+        } 
+        return path;
+    }
+    
+    public static void main (String[] args) { // todo remove as testing
+        StringBuffer err = new StringBuffer();
+        StringBuffer out = new StringBuffer();
+        for (int i = 0; i < args.length; i++) {
+            String[] names = classesIn(new File(args[i]));
+            System.err.println(args[i] + " -> " + render(names));
+            loadClasses(names, out, err); 
+        }
+        if (0 < err.length()) {
+            System.err.println(err.toString());
+        }
+        if (0 < out.length()) {
+            System.out.println(out.toString());
+        }
+    } 
+
+    public static String render (String[] args) { // todo move as testing
+        if ((null == args) || (1 > args.length)) {
+            return "[]";
+        }
+        boolean longFormat = (args.length < 10);
+        String sep = (longFormat ? ", " : "\n\t");
+        StringBuffer sb = new StringBuffer();
+        if (!longFormat) sb.append("[");
+        for (int i = 0; i < args.length; i++) {
+            if (0 < i) sb.append(sep);
+            sb.append(args[i]);
+        } 
+        sb.append(longFormat ? "\n" : "]");
+        return sb.toString();
+    } 
+    
+   
+    /**
+     * @param thrown the Throwable to render
+     */
+    public static String debugStr(Throwable thrown) {
+        if (null == thrown) {
+            return "((Throwable) null)";
+        } else if (thrown instanceof InvocationTargetException) {
+            return debugStr(((InvocationTargetException)thrown).getTargetException());
+        } else if (thrown instanceof AbortException) {
+            IMessage m = ((AbortException) thrown).getIMessage();
+            if (null != m) {
+                return "" + m;
+            }
+        }
+        StringWriter buf = new StringWriter();
+        PrintWriter writer = new PrintWriter(buf);
+        writer.println(thrown.getMessage());
+        thrown.printStackTrace(writer);
+        try { buf.close(); } 
+        catch (IOException ioe) {} 
+        return buf.toString();
+    }
+
+    /**
+     * <code>debugStr(o, false);</code>
+     * @param source the Object to render
+     */
+    public static String debugStr(Object o) {
+        return debugStr(o, false);
+    }
+    
+    /**
+     * Render standard debug string for an object in normal, default form.
+     * @param source the Object to render
+     * @param recurse if true, then recurse on all non-primitives unless rendered
+     */
+    public static String debugStr(Object o, boolean recurse) {
+        if (null == o) {
+            return "null";
+        } else if (recurse) {
+            ArrayList rendering = new ArrayList();
+            rendering.add(o);
+            return debugStr(o, rendering);
+        } else {
+            Class c = o.getClass();
+            Field[] fields = c.getDeclaredFields();
+            Object[] values = new Object[fields.length];
+            String[] names = new String[fields.length];
+            for (int i = 0; i < fields.length; i++) {
+                               Field field = fields[i];
+                               names[i] = field.getName();
+                try {
+                    values[i] = field.get(o);
+                    if (field.getType().isArray()) {
+                        List list = org.aspectj.util.LangUtil.arrayAsList((Object[]) values[i]);
+                        values[i] = list.toString();
+                    }
+                } catch (IllegalAccessException e) {
+                    values[i] = "<IllegalAccessException>";
+                }
+                       }
+            return debugStr(c, names, values);
+        }
+    }
+
+    /**
+     * recursive variant avoids cycles.
+     * o added to rendering before call.
+     */
+    private static String debugStr(Object o, ArrayList rendering) {
+        if (null == o) {
+            return "null";
+        } else if (!rendering.contains(o)) {
+            throw new Error("o not in rendering");
+        }
+        Class c = o.getClass();
+        if (c.isArray()) {
+            Object[] ra = (Object[]) o;
+            StringBuffer sb = new StringBuffer();
+            sb.append("[");
+            for (int i = 0; i < ra.length; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                rendering.add(ra[i]);
+                               sb.append(debugStr(ra[i], rendering));
+                       }
+            sb.append("]");
+            return sb.toString();
+        }
+        Field[] fields = nonStaticFields(c.getFields());
+        Object[] values = new Object[fields.length];
+        String[] names = new String[fields.length];
+        for (int i = 0; i < fields.length; i++) {
+            Field field = fields[i];
+            names[i] = field.getName();
+            // collapse to String
+            Object value = privilegedGetField(field,o);
+            if (null == value) { 
+                values[i] = "null";
+            } else if (rendering.contains(value)) {
+                values[i] = "<recursion>";
+            } else {
+                rendering.add(value);
+                values[i] = debugStr(value, rendering);
+            }
+        }
+        return debugStr(c, names, values);
+    }
+
+    /** incomplete - need protection domain */
+    private static Object privilegedGetField(final Field field, final Object o) {
+        try {
+            return AccessController.doPrivileged(new PrivilegedExceptionAction() {
+                public Object run() {
+                    try {
+                        return field.get(o);
+                    } catch(IllegalAccessException e) {
+                        return "<IllegalAccessException>";
+                    }
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            return "<IllegalAccessException>";
+        }               
+    }
+    
+    private static Field[] nonStaticFields(Field[] fields) {
+        if (null == fields) {
+            return new Field[0];
+        }
+        int to = 0;
+        int from = 0;
+        while (from < fields.length) {
+            if (!Modifier.isStatic(fields[from].getModifiers())) {
+                if (to != from) {
+                    fields[to] = fields[from];
+                }
+                to++;
+            }
+            from++;
+        }
+        if (to < from) {
+            Field[] result = new Field[to];
+            if (to > 0) {
+                System.arraycopy(fields, 0, result, 0, to);
+            }
+            fields = result;
+        }
+        return fields;
+    }
+
+    /** <code> debugStr(source, names, items, null, null, null, null)<code> */ 
+    public static String debugStr(Class source, String[] names, Object[] items) {
+        return debugStr(source, null, names, null, items, null, null);
+    }
+
+    /**
+     * Render standard debug string for an object.
+     * This is the normal form and an example with the default values:<pre>
+     * {className}{prefix}{{name}{infix}{value}{delimiter}}..{suffix}
+     * Structure[head=root, tail=leaf]</pre>
+     * Passing null for the formatting entries provokes the default values,
+     * so to print nothing, you should pass "".  Default values:<pre>
+     *    prefix: "["   SPLIT_START
+     *     infix: "="
+     * delimiter: ", "  SPLIT_DELIM
+     *    suffix: "]"   SPLIT_END
+     * @param source the Class prefix to render unqualified - omitted if null
+     * @param names the String[] (field) names of the items - omitted if null
+     * @param items the Object[] (field) values
+     * @param prefix the String to separate classname and start of name/values
+     * @param delimiter the String to separate name/value instances
+     * @param infix the String to separate name and value
+     *         used only if both name and value exist
+     * @param suffix the String to delimit the end of the name/value instances
+     *         used only if classname exists
+     */
+    public static String debugStr(Class source, String prefix, String[] names, 
+        String infix, Object[] items, String delimiter,  String suffix) {
+
+        if (null == delimiter) {
+            delimiter = SPLIT_DELIM;
+        }
+        if (null == prefix) {
+            prefix = SPLIT_START;
+        }
+        if (null == infix) {
+            infix = "=";
+        }
+        if (null == suffix) {
+            suffix = SPLIT_END;
+        }
+        StringBuffer sb = new StringBuffer();
+        if (null != source) {
+            sb.append(org.aspectj.util.LangUtil.unqualifiedClassName(source));
+        }
+        sb.append(prefix);
+        if (null == names) {
+            names = NONE;
+        }
+        if (null == items) {
+            items = NONE;
+        }
+        final int MAX 
+            = (names.length > items.length ? names.length : items.length);
+        for (int i = 0; i < MAX; i++) {
+                       if (i > 0) {
+                sb.append(delimiter);
+            }
+            if (i < names.length) {
+                sb.append(names[i]);
+            }
+            if (i < items.length) {
+                if (i < names.length) {
+                    sb.append(infix);
+                }
+                sb.append(items[i] + "");
+            }            
+               }
+        sb.append(suffix);
+        return sb.toString();
+    }
+
+    /** 
+     * @return a String with the unqualified class name of the object (or "null")
+     */
+    public static String unqualifiedClassName(Object o) {
+        return unqualifiedClassName(null == o ? null : o.getClass());
+    }
+
+    /** 
+     * @return a String with the unqualified class name of the class (or "null")
+     */
+    public static String unqualifiedClassName(Class c) {
+        if (null == c) {
+            return "null";
+        }
+        String name = c.getName();
+        int loc = name.lastIndexOf(".");
+        if (-1 != loc)
+            name = name.substring(1 + loc);
+        return name;
+    }
+    
+    /** 
+     * Calculate exact diffs and report missing and extra items.
+     * This assumes the input List are not modified concurrently.
+     * @param expectedListIn the List of expected results - treated as empty if null
+     * @param actualListIn the List of actual results - treated as empty if null
+     * @param extraListOut the List for any actual results not expected - ignored if null
+     * @param missingListOut the List for any expected results not found - ignored if null
+     * */
+       public static void makeDiffs(
+               List expectedListIn,
+               List actualListIn,
+               List missingListOut,
+        List extraListOut) {
+               if ((null == missingListOut) && (null == extraListOut)) {
+                       return;
+               }
+               if (null == expectedListIn) {
+                       expectedListIn = Collections.EMPTY_LIST;
+               }
+               if (null == actualListIn) {
+                       actualListIn = Collections.EMPTY_LIST;
+               }
+        if ((0 == actualListIn.size()) && (0 == expectedListIn.size()) ) {
+            return;
+        }
+               BitSet actualExpected = new BitSet();
+               for (int i = 0; i < expectedListIn.size(); i++) {
+                       Object expect = expectedListIn.get(i);
+                       int loc = actualListIn.indexOf(expect);
+                       if (-1 == loc) {
+                               if (null != missingListOut) {
+                                       missingListOut.add(expect);
+                               }
+                       } else {
+                               actualExpected.set(loc);
+                       }
+               }
+               if (null != extraListOut) {
+                       for (int i = 0; i < actualListIn.size(); i++) {
+                               if (!actualExpected.get(i)) {
+                                       extraListOut.add(actualListIn.get(i));
+                               }
+                       }
+               }
+       }
+    // XXX unit test for makeSoftDiffs
+    /** 
+     * Calculate potentially "soft" diffs using 
+     * Comparator.compare(expected, actual).
+     * This shallow-copies and sorts the input Lists.
+     * @param expectedListIn the List of expected results - treated as empty if null
+     * @param actualListIn the List of actual results - treated as empty if null
+     * @param extraListOut the List for any actual results not expected - ignored if null
+     * @param missingListOut the List for any expected results not found - ignored if null
+     * @param comparator the Comparator for comparisons - not null
+     * @throws IllegalArgumentException if comp is null
+     */
+    public static void makeSoftDiffs(  // XXX no intersect or union on collections???
+        List expectedListIn,
+        List actualListIn,
+        List missingListOut,
+        List extraListOut,
+        Comparator comparator) {
+        if ((null == missingListOut) && (null == extraListOut)) {
+            return;
+        }
+        if (null == comparator) {
+            throw new IllegalArgumentException("null comparator");
+        }
+        if (null == expectedListIn) {
+            expectedListIn = Collections.EMPTY_LIST;
+        }
+        if (null == actualListIn) {
+            actualListIn = Collections.EMPTY_LIST;
+        }
+        if ((0 == actualListIn.size()) && (0 == expectedListIn.size()) ) {
+            return;
+        }
+        
+        ArrayList expected = new ArrayList();
+        expected.addAll(expectedListIn);
+        Collections.sort(expected, comparator);
+        Iterator expectedIter = expected.iterator();
+        Object exp = null;
+        
+        ArrayList actual = new ArrayList();
+        actual.addAll(actualListIn);
+        Collections.sort(actual, comparator);        
+        Iterator actualIter = actual.iterator();        
+        Object act = null;
+        
+        while (((null != act) || actualIter.hasNext())
+             && ((null != exp) || expectedIter.hasNext())) {
+            if (null == act) {
+                act = actualIter.next();
+            }
+            if (null == exp) {
+                exp = expectedIter.next();
+            }
+            int diff = comparator.compare(exp, act);
+            if (0 > diff) {          // exp < act
+                if (null != missingListOut) {
+                    missingListOut.add(exp);
+                    exp = null;
+                }
+            } else if (0 < diff) {   // exp > act
+                if (null != extraListOut) {
+                    extraListOut.add(act);
+                    act = null;
+                }
+            } else { // got match of actual to expected
+                // absorb all actual matching expected (duplicates)
+                while ((0 == diff) && actualIter.hasNext()) {
+                    act = actualIter.next();
+                    diff = comparator.compare(exp, act);
+                }
+                if (0 == diff) {
+                    act = null;
+                }
+                exp = null;
+            }
+        }
+        if (null != missingListOut) {
+             if (null != exp) {
+                missingListOut.add(exp);
+             }
+            while (expectedIter.hasNext()) {
+                 missingListOut.add(expectedIter.next());
+            }
+        }
+        if (null != extraListOut) {
+            if (null != act) {
+                extraListOut.add(act);
+            }
+            while (actualIter.hasNext()) {
+                 extraListOut.add(actualIter.next());
+            }
+        }
+    }    
+    public static class FlattenSpec {
+        /** 
+         * This tells unflatten(..) to throw IllegalArgumentException
+         * if it finds two contiguous delimiters.
+         */
+        public static final String UNFLATTEN_EMPTY_ERROR 
+            = "empty items not permitted when unflattening";
+        /** 
+         * This tells unflatten(..) to skip empty items when unflattening
+         * (since null means "use null")
+         */
+        public static final String UNFLATTEN_EMPTY_AS_NULL 
+            = "unflatten empty items as null";
+            
+        /** 
+         * This tells unflatten(..) to skip empty items when unflattening
+         * (since null means "use null")
+         */
+        public static final String SKIP_EMPTY_IN_UNFLATTEN
+            = "skip empty items when unflattening";
+        
+        /** 
+         * For Ant-style attributes: "item,item" (with escaped commas).
+         * There is no way when unflattening to distinguish 
+         * values which were empty from those which were null,
+         * so all are unflattened as empty.
+         */
+        public static final FlattenSpec COMMA
+            = new FlattenSpec(null, "", "\\", ",", null, "") {
+                public String toString() { return "FlattenSpec.COMMA"; }
+            };
+            
+        /** this attempts to mimic ((List)l).toString() */
+        public static final FlattenSpec LIST
+            = new FlattenSpec("[", "", null, ", ", "]", UNFLATTEN_EMPTY_ERROR) {
+                public String toString() { return "FlattenSpec.LIST"; }
+            };
+            
+        /** how toString renders null values */
+        public static final String NULL = "<null>";
+        private static String r(String s) {
+            return (null == s ? NULL : s);
+        }
+
+        public final String prefix;
+        public final String nullFlattened;
+        public final String escape;
+        public final String delim;
+        public final String suffix;
+        public final String emptyUnflattened;
+        private transient String toString; 
+
+        public FlattenSpec(
+            String prefix, 
+            String nullRendering, 
+            String escape,
+            String delim, 
+            String suffix,
+            String emptyUnflattened) {
+            this.prefix = prefix;
+            this.nullFlattened = nullRendering;
+            this.escape = escape;
+            this.delim = delim;
+            this.suffix = suffix;
+            this.emptyUnflattened = emptyUnflattened;
+            throwIaxIfNull(emptyUnflattened, "use UNFLATTEN_EMPTY_AS_NULL");
+        }
+        
+        public String toString() {
+            if (null == toString) {
+                toString = "FlattenSpec("
+                    + "prefix=" + r(prefix)
+                    + ", nullRendering=" + r(nullFlattened)
+                    + ", escape=" + r(escape)
+                    + ", delim=" + r(delim)
+                    + ", suffix=" + r(suffix)
+                    + ", emptyUnflattened=" + r(emptyUnflattened)
+                    + ")";
+            }
+            return toString;
+        }
+    }
+} // class LangUtil
+
+//  --------- java runs using Ant
+//     /**
+//      * Run a Java command separately.
+//      * @param className the fully-qualified String name of the class 
+//     *         with the main method to run
+//      * @param classpathFiles the File to put on the classpath
+//      * @param args to the main method of the class
+//     * @param outSink the PrintStream for the output stream - may be null
+//      */
+//     public static void oldexecuteJava(
+//             String className,
+//             File[] classpathFiles,
+//             String[] args,
+//        PrintStream outSink) {
+//        Project project = new Project();
+//        project.setName("LangUtil.executeJava(" + className + ")");
+//        Path classpath = new Path(project, classpathFiles[0].getAbsolutePath());
+//        for (int i = 1; i < classpathFiles.length; i++) {
+//                     classpath.addExisting(new Path(project, classpathFiles[i].getAbsolutePath()));
+//             }        
+//
+//        Commandline cmds = new Commandline();
+//        cmds.addArguments(new String[] {className});
+//        cmds.addArguments(args);      
+//
+//        ExecuteJava runner = new ExecuteJava();
+//        runner.setClasspath(classpath);
+//        runner.setJavaCommand(cmds);        
+//        if (null != outSink) {
+//            runner.setOutput(outSink); // XXX todo
+//        }
+//        runner.execute(project);
+//     }
+
+//     public static void executeJava(
+//        String className,
+//        File dir,
+//        File[] classpathFiles,
+//        String[] args,
+//        PrintStream outSink) {
+//        StringBuffer sb = new StringBuffer();
+//        
+//        sb.append("c:/apps/jdk1.3.1/bin/java.exe -classpath \"");
+//        for (int i = 0; i < classpathFiles.length; i++) {
+//            if (i < 0) {
+//                sb.append(";");
+//            }
+//            sb.append(classpathFiles[i].getAbsolutePath());            
+//        }        
+//        sb.append("\" -verbose " + className);
+//        for (int i = 0; i < args.length; i++) {
+//            sb.append(" " + args[i]);
+//             }
+//        Exec exec = new Exec();
+//        Project project = new Project();
+//        project.setProperty("ant.home", "c:/home/wes/aj/aspectj/modules/lib/ant");
+//        System.setProperty("ant.home", "c:/home/wes/aj/aspectj/modules/lib/ant");
+//        exec.setProject(new Project());
+//        exec.setCommand(sb.toString());
+//        exec.setDir(dir.getAbsolutePath());
+//        exec.execute();
+//    }
+//     public static void execJavaProcess(
+//        String className,
+//        File dir,
+//        File[] classpathFiles,
+//        String[] args,
+//        PrintStream outSink) throws Throwable {
+//        StringBuffer sb = new StringBuffer();
+//        
+//        sb.append("c:\\apps\\jdk1.3.1\\bin\\java.exe -classpath \"");
+//        for (int i = 0; i < classpathFiles.length; i++) {
+//            if (i > 0) {
+//                sb.append(";");
+//            }
+//            sb.append(classpathFiles[i].getAbsolutePath());            
+//        }        
+//        sb.append("\" -verbose " + className);
+//        for (int i = 0; i < args.length; i++) {
+//            sb.append(" " + args[i]);
+//        }
+//        String command = sb.toString();
+//        System.err.println("launching process: " + command);
+//        Process process = Runtime.getRuntime().exec(command);
+//        // huh? err/out
+//        InputStream errStream = null; 
+//        InputStream outStream = null; 
+//        Throwable toThrow = null;
+//        int result = -1;
+//        try {
+//            System.err.println("waiting for process: " + command);
+//            errStream = null; // process.getErrorStream();
+//            outStream = null; // process.getInputStream(); // misnamed - out
+//            result = process.waitFor();
+//            System.err.println("Done waiting for process: " + command);
+//            process.destroy();
+//        } catch (Throwable t) {
+//            toThrow = t;
+//        } finally {
+//            if (null != outStream) {
+//                FileUtil.copy(outStream, System.out, false);
+//                try { outStream.close(); }
+//                catch (IOException e) {}
+//            }
+//            if (null != errStream) {
+//                FileUtil.copy(errStream, System.err, false);
+//                try { errStream.close(); }
+//                catch (IOException e) {}
+//            }
+//        }
+//        if (null != toThrow) {
+//            throw toThrow;
+//        }
+//    }
+//        try {
+//            // show the command
+//            log(command, Project.MSG_VERBOSE);
+//
+//            // exec command on system runtime
+//            Process proc = Runtime.getRuntime().exec(command);
+//
+//            if (out != null)  {
+//                fos = new PrintWriter(new FileWriter(out));
+//                log("Output redirected to " + out, Project.MSG_VERBOSE);
+//            }
+//
+//            // copy input and error to the output stream
+//            StreamPumper inputPumper =
+//                new StreamPumper(proc.getInputStream(), Project.MSG_INFO);
+//            StreamPumper errorPumper =
+//                new StreamPumper(proc.getErrorStream(), Project.MSG_WARN);
+//
+//            // starts pumping away the generated output/error
+//            inputPumper.start();
+//            errorPumper.start();
+//
+//            // Wait for everything to finish
+//            proc.waitFor();
+//            inputPumper.join();
+//            errorPumper.join();
+//            proc.destroy();
+//
+//            // close the output file if required
+//            logFlush();
+//
+//            // check its exit value
+//            err = proc.exitValue();
+//            if (err != 0) {
+//                if (failOnError) {
+//                    throw new BuildException("Exec returned: " + err, getLocation());
+//                } else {
+//                    log("Result: " + err, Project.MSG_ERR);
+//                }
+//            }
+//        } catch (IOException ioe) {
+//            throw new BuildException("Error exec: " + command, ioe, getLocation());
+//        } catch (InterruptedException ex) {}
+//
+
+
diff --git a/testing/src/org/aspectj/testing/util/LineReader.java b/testing/src/org/aspectj/testing/util/LineReader.java
new file mode 100644 (file)
index 0000000..67f9710
--- /dev/null
@@ -0,0 +1,204 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * LineReader.java created on May 3, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.util.ArrayList;
+
+/** LineNumberReader which absorbs our lines and renders as file:line */
+public class LineReader extends LineNumberReader {
+    /** delimited multi-line output of readToBlankLine */
+    public static final String RETURN= "\n\r";
+    
+    private static final String[] NONE = new String[0];
+    private static final String cSCRIPT = "#";
+    private static final String cJAVA = "//";
+    private static final String[] TESTER_LEAD = new String[] {cSCRIPT, cJAVA};
+    
+    /** 
+     * Convenience factory for tester suite files
+     * @return null if IOException or IllegalArgumentException thrown
+     */
+    public static final LineReader createTester(File file) {
+        return create(file, TESTER_LEAD, null);
+    }
+    
+    /** 
+     * convenience factory 
+     * @return null if IOException or IllegalArgumentException thrown
+     */
+    public static final LineReader create(File file, 
+        String[] leadComments, String[] eolComments) {
+        try {
+            FileReader reader = new FileReader(file);
+            return new LineReader(reader, file, leadComments, eolComments);
+        } catch (IllegalArgumentException e) {
+        } catch (IOException e) {
+        }
+        return null;
+    }
+    
+    final private File file;
+    final private String[] eolComments;
+    final private String[] leadComments;
+    
+    /**
+     * @param file the File used to open the FileReader
+     * @param leadComments the String[] to be taken as the start of
+     * comments when they are the first non-blank text on a line -
+     * pass null to signal none.
+     * @param leadComments the String[] to be taken as the start of
+     * comment anywhere on a line - pass null to signal none.
+     *@throws IllegalArgumentException if any String in
+     * leadComments or eolComments is null.
+     */
+    public LineReader(FileReader reader, File file, 
+        String[] leadComments, String[] eolComments) {
+        super(reader); 
+        this.file = file;
+        this.eolComments = normalize(eolComments);
+        this.leadComments = normalize(leadComments);
+    }
+    public LineReader(FileReader reader, File file) {
+        this(reader, file, null, null);
+    }
+    
+    /** @return file:line */
+    public String toString() {
+        return file.getPath() + ":" + getLineNumber();
+    }
+    
+    /** @return underlying file */
+    public File getFile() { return file; }
+
+    /**
+     * Reader first..last (inclusive) and return in String[].
+     * This will return (1+(last-first)) elements only if this
+     * reader has not read past the first line and there are last lines
+     * and there are no IOExceptions during reads.
+     * @param first the first line to read - if negative, use 0
+     * @param last the last line to read (inclusive) 
+     *         - if less than first, use first
+     * @return String[] of first..last (inclusive) lines read or 
+     */
+    public String[] readLines(int first, int last) {
+        if (0 > first) first = 0;
+        if (first > last) last = first;
+        ArrayList list = new ArrayList();
+        try {
+            String line = null;
+            while (getLineNumber() < first) { 
+                line = readLine();
+                if (null == line) {
+                    break; 
+                }
+            }
+            if (getLineNumber() > first) { 
+                // XXX warn? something else read past line
+            }
+            if ((null != line) && (first == getLineNumber())) {
+                list.add(line);
+                while (last >= getLineNumber()) {
+                    line = readLine();
+                    if (null == line) {
+                        break;
+                    }
+                    list.add(line);
+                }
+            }
+        } catch (IOException e) {
+            return NONE;
+        }
+        return (String[]) list.toArray(NONE);
+    }
+    
+    /** Skip to next blank line 
+     * @return the String containing all lines skipped (delimited with RETURN)
+     */
+    public String readToBlankLine() throws IOException {
+        StringBuffer sb = new StringBuffer();
+        String input;
+        while (null != (input = nextLine(false))) { // get next empty line to restart
+            sb.append(input);
+            sb.append(RETURN);// XXX verify/ignore/correct
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Get the next line from the input stream, stripping eol and
+     * leading comments.
+     * If emptyLinesOk is true, then this reads past lines which are
+     * empty after omitting comments and trimming until the next non-empty line.
+     * Otherwise, this returns null on reading an empty line.
+     * (The input stream is not exhausted until this
+     * returns null when emptyLines is true.)
+     * @param emptyLinesOk if false, return null if the line is empty
+     * @return next non-null, non-empty line in reader,
+     * ignoring comments
+     */
+    public String nextLine(boolean emptyLinesOk) throws IOException {
+        int len = 0;
+        String result = null;
+        do {
+            result = readLine();
+            if (result == null)
+                return null;
+            result = result.trim();
+            for (int i = 0; i < eolComments.length; i++) {
+                int loc = result.indexOf(eolComments[i]);
+                if (-1 != loc) {
+                    result = result.substring(0, loc);
+                    break;
+                }
+            }
+            len = result.length();
+            if (0 < len) {
+                for (int i = 0; i < leadComments.length; i++) {
+                    if (result.startsWith(leadComments[i])) {
+                        result = "";
+                        break;
+                    }
+                }
+                len = result.length();
+            }
+            len = result.length();
+            if (!emptyLinesOk && (0 == len))
+                return null;
+        } while (0 == len);
+        return result;
+    }
+
+    private String[] normalize(String[] input) {
+        if ((null == input) || (0 == input.length)) return NONE;
+        String[] result = new String[input.length];
+        System.arraycopy(input, 0, result, 0, result.length);
+        for (int i = 0; i < result.length; i++) {
+            if ((null == result[i]) || (0 == result[i].length())) {
+                throw new IllegalArgumentException("empty input at [" + i + "]");
+            }
+        }
+        return result;
+    }
+
+}
+
diff --git a/testing/src/org/aspectj/testing/util/Node.java b/testing/src/org/aspectj/testing/util/Node.java
new file mode 100644 (file)
index 0000000..11d197e
--- /dev/null
@@ -0,0 +1,162 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * Node.java created on May 14, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 
+  * A node in a tree containing other Node or SpecElements items.
+  */
+public class Node { // XXX render
+    public static final Node[] EMPTY_NODES = new Node[0];
+    public static final Object[] EMPTY_ITEMS = new Object[0];
+
+    /** 
+     * Visit all the SpecElements (and Node) reachable from node
+     * in depth-first order, halting if checker objects.
+     * @param node the Node to pass to checker
+     * @param itemChecker the ObjectChecker to pass items to
+     * @param nodeVisitor if not null, then use instead of recursing
+     * @return false on first objection, true otherwise
+     * @throws IllegalArgumentExcpetion if checker is null
+     */
+    public static final boolean visit(Node node, ObjectChecker itemChecker,
+                                         ObjectChecker nodeVisitor) {
+        if (null == node) {
+            return (null == itemChecker ? true : itemChecker.isValid(null));
+        }
+        boolean result = true;
+
+        Node[] nodes = node.getNodes();
+        for (int i = 0; result && (i < nodes.length); i++) {
+            result = (null == nodeVisitor 
+                    ? visit(nodes[i], itemChecker, null)
+                    : nodeVisitor.isValid(nodes[i]));
+        }
+        if (result) {
+            Object[] elements = node.getItems();
+            for (int i = 0; result && (i < elements.length); i++) {
+                result = itemChecker.isValid(elements[i]);
+            }
+        }
+
+        return result;
+    }
+
+    public final String name;
+    public final Class type;
+    public final Object key;
+    protected final Object[] typeArray;
+    protected final List nodes;
+    protected final List items;
+
+    public Node() {
+        this("Node");
+    }
+
+    public Node(String name) {
+        this(name, null);
+    }
+
+    /** use the name as the key */
+    public Node(String name, Class type) {
+        this(name, type, name);
+    }
+    /**  */
+    public Node(String name, Class type, Object key) {    
+        if (null == name) {
+            throw new IllegalArgumentException("null name");
+        }
+        if (null == key) {
+            throw new IllegalArgumentException("null key");
+        }
+        this.name = name;
+        this.type = type;
+        this.key = key;
+        nodes = new ArrayList();
+        items = new ArrayList();
+        if (type == null) {
+            type = Object.class;
+        }
+        typeArray = (Object[]) Array.newInstance(type, 0);
+    }
+
+    /** 
+     * clear all items and nodes.
+     */
+    public void clear() { // XXX synchronize
+        nodes.clear();
+        items.clear();
+    }
+    
+    /**
+     * Add item to list of items
+     * unless it is null, of the wrong type, or the collection fails to add
+     * @return true if added
+     */
+    public boolean add(Object item) {
+        if (null == item)
+            throw new IllegalArgumentException("null item");
+        if ((null != type) && (!type.isAssignableFrom(item.getClass()))) {
+            return false;
+        }
+        return items.add(item);
+    }
+
+    /**
+     * Add node to list of nodes
+     * unless it is null, of the wrong type, or the collection fails to add
+     * @return true if added
+     */
+    public boolean addNode(Node node) {
+        if (null == node) {
+            throw new IllegalArgumentException("null node");
+        }
+        return nodes.add(node);
+    }
+
+    /**
+     * Get the current list of nodes - never null
+     */
+    public Node[] getNodes() {
+        if ((null == nodes) || (1 > nodes.size())) {
+            return EMPTY_NODES;
+        }
+        return (Node[]) nodes.toArray(EMPTY_NODES);
+    }
+
+    /**
+     * Get the current list of items - never null
+     * @return items in current list, cast to type[] if type was not null
+     */
+    public Object[] getItems() {
+        if ((null == items) || (1 > items.size())) {
+            return EMPTY_ITEMS;
+        }
+        return items.toArray(typeArray);
+    }
+
+    /** @return name */
+    public String toString() {
+        return name;
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/NullPrintStream.java b/testing/src/org/aspectj/testing/util/NullPrintStream.java
new file mode 100644 (file)
index 0000000..7ba8e28
--- /dev/null
@@ -0,0 +1,95 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * NullPrintStream.java created on May 29, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+/**
+ * Ignore any output to a NullPrintStream.
+ * Clients use singleton NULL_PrintStream or NULL_OutputStream.
+ * @author isberg
+ */
+public final class NullPrintStream extends PrintStream {
+
+    public static final OutputStream NULL_OutputStream = NullOutputStream.ME;
+    public static final PrintStream NULL_PrintStream = new NullPrintStream();
+
+    private NullPrintStream() {
+        super(NULL_OutputStream);
+    }
+    public void write(int b) {
+    }
+    public void write(byte[] b) {
+    }
+    public void write(byte[] b, int off, int len) {
+    }
+    public void print(boolean arg0) {
+    }
+    public void print(char arg0) {
+    }
+    public void print(char[] arg0) {
+    }
+    public void print(double arg0) {
+    }
+    public void print(float arg0) {
+    }
+    public void print(int arg0) {
+    }
+    public void print(long arg0) {
+    }
+    public void print(Object arg0) {
+    }
+    public void print(String arg0) {
+    }
+    public void println() {
+    }
+    public void println(boolean arg0) {
+    }
+    public void println(char arg0) {
+    }
+    public void println(char[] arg0) {
+    }
+    public void println(double arg0) {
+    }
+    public void println(float arg0) {
+    }
+    public void println(int arg0) {
+    }
+    public void println(long arg0) {
+    }
+    public void println(Object arg0) {
+    }
+    public void println(String arg0) {
+    }
+
+}
+
+final class NullOutputStream extends OutputStream {
+    static final OutputStream ME = new NullOutputStream();
+
+    private NullOutputStream() {
+    }
+    public void write(int b) {
+    }
+    public void write(byte[] b) {
+    }
+    public void write(byte[] b, int off, int len) {
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/ObjectChecker.java b/testing/src/org/aspectj/testing/util/ObjectChecker.java
new file mode 100644 (file)
index 0000000..e7d4275
--- /dev/null
@@ -0,0 +1,38 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+/**
+  * Check input for validity. 
+  */
+public interface ObjectChecker {
+    /** this returns true for any input, even if null */
+    public static final ObjectChecker ANY = new ObjectChecker() {
+        public final boolean isValid(Object input) { return true; }
+        public final String toString() { return "ObjectChecker.ANY"; }
+    };
+    /** this returns true for any non-null object */
+    public static final ObjectChecker NOT_NULL = new ObjectChecker() {
+        public boolean isValid(Object input) { return (null != input); }
+        public String toString() { return "ObjectChecker.MOT_NULL"; }
+    };
+    
+    /**
+     * Check input for validity. 
+     * @param input the Object to check
+     * @return true if input is ok 
+     */
+    public boolean isValid(Object input);
+}
diff --git a/testing/src/org/aspectj/testing/util/ProxyPrintStream.java b/testing/src/org/aspectj/testing/util/ProxyPrintStream.java
new file mode 100644 (file)
index 0000000..378f58e
--- /dev/null
@@ -0,0 +1,104 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+/** Wrap a delegate PrintStream, permitting output to be suppressed. */
+public class ProxyPrintStream extends PrintStream {
+
+    private final PrintStream delegate;
+    private boolean hiding;
+    public ProxyPrintStream(PrintStream delegate ) {
+        super(NullPrintStream.NULL_OutputStream);
+        LangUtil.throwIaxIfNull(delegate, "delegate");
+        this.delegate = delegate;
+    }
+    public void hide() {
+        hiding = true;
+    }
+    public void show() {
+        hiding = false;
+    }
+    public boolean isHiding() {
+        return hiding;
+    }
+    public void write(int b) {
+        if (!hiding) delegate.write(b);
+    }
+    public void write(byte[] b) throws IOException {
+        if (!hiding) delegate.write(b);
+    }
+    public void write(byte[] b, int off, int len) {
+        if (!hiding) delegate.write(b, off, len);
+    }
+    public void print(boolean arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(char arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(char[] arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(double arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(float arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(int arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(long arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(Object arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void print(String arg0) {
+        if (!hiding) delegate.print(arg0);
+    }
+    public void println() {
+        if (!hiding) delegate.println();
+    }
+    public void println(boolean arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(char arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(char[] arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(double arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(float arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(int arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(long arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(Object arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+    public void println(String arg0) {
+        if (!hiding) delegate.println(arg0);
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/RunUtils.java b/testing/src/org/aspectj/testing/util/RunUtils.java
new file mode 100644 (file)
index 0000000..febfac4
--- /dev/null
@@ -0,0 +1,368 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.bridge.MessageUtil.IMessageRenderer;
+import org.aspectj.testing.harness.bridge.AbstractRunSpec;
+import org.aspectj.testing.harness.bridge.IRunSpec;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.RunValidator;
+import org.aspectj.util.LangUtil;
+
+import java.io.PrintStream;
+import java.util.Iterator;
+
+/**
+ * 
+ */
+public class RunUtils {
+
+    /** enable verbose for this an any related AbstractRunSpec children */
+    public static void enableVerbose(AbstractRunSpec spec) { // instanceof hack
+        LangUtil.throwIaxIfNull(spec, "spec");
+        spec.runtime.setVerbose(true);
+        for (Iterator iter = spec.getChildren().iterator(); iter.hasNext();) {
+                       IRunSpec child = (IRunSpec) iter.next();
+                       if (child instanceof AbstractRunSpec) {
+                enableVerbose((AbstractRunSpec) child);
+            }
+               }
+    }
+    
+    /** 
+     * Calculate failures for this status.
+     * If the input status has no children and failed, the result is 1.
+     * If it has children and recurse is false, then
+     * the result is the number of children whose status has failed
+     * (so a failed status with some passing and no failing children
+     *  will return 0).
+     * If it has children and recurse is true,
+     * then return the number of leaf failures in the tree,
+     * ignoring (roll-up) node failures.
+     * @return number of failures in children of this status 
+     */
+    public static int numFailures(IRunStatus status, boolean recurse) {
+        int numFails = 0;
+        IRunStatus[] children = status.getChildren();
+        int numChildren = (null == children? 0 : children.length);
+        if (0 == numChildren) {
+            if (!RunValidator.NORMAL.runPassed(status)) {
+                return 1;
+            }
+        } else { 
+            int i = 0;
+            for (int j = 0; j < children.length; j++) {
+                if (recurse) {
+                    numFails += numFailures(children[j], recurse);
+                } else {
+                    if (!RunValidator.NORMAL.runPassed(children[j])) {
+                        numFails++;
+                    }
+                }
+            }
+        }
+        return numFails;
+    }
+    
+    // ------------------------ printing status
+    public static void printShort(PrintStream out, IRunStatus status) {
+        if ((null == out) || (null == status)) {
+            return;
+        }
+        printShort(out, "", status);
+    }
+
+    public static void printShort(PrintStream out, String prefix, IRunStatus status) {
+        int numFails = numFailures(status, true);
+        String fails = (0 == numFails ? "" : " - " + numFails + " failures");
+        out.println(prefix + toShortString(status) + fails);
+        IRunStatus[] children = status.getChildren();
+        int numChildren = (null == children? 0 : children.length);
+        if (0 < numChildren) {
+            int i = 0;
+            for (int j = 0; j < children.length; j++) {
+                printShort(out, prefix + "[" + LangUtil.toSizedString(i++, 3) + "]: ", children[j]);
+                if (!RunValidator.NORMAL.runPassed(children[j])) {
+                    numFails++;
+                }
+            }
+        }
+    }
+
+    public static void print(PrintStream out, IRunStatus status) {
+        if ((null == out) || (null == status)) {
+            return;
+        }
+        print(out, "", status);
+    }
+    
+    public static void print(PrintStream out, String prefix, IRunStatus status) {
+        print(out, prefix, status, MessageUtil.MESSAGE_ALL, MessageUtil.PICK_ALL);
+    }     
+
+    public static void print(PrintStream out, String prefix, IRunStatus status,
+        IMessageRenderer renderer, IMessageHandler selector) {
+        String label = status.getIdentifier()         
+                    + (status.runResult() ? "PASS" : "FAIL");
+        out.println(prefix + label);
+        out.println(prefix + debugString(status));
+        IMessageHolder messageHolder = status;
+        if ((null != messageHolder) && (0 < messageHolder.numMessages(null, true))) {
+            MessageUtil.print(out, messageHolder, prefix, renderer, selector);
+        }   
+        Throwable thrown = status.getThrown();
+        if (null != thrown) {
+            out.println(prefix + "--- printing stack trace for thrown");
+            thrown.printStackTrace(out);
+        } 
+        IRunStatus[] children = status.getChildren();
+        int numChildren = (null == children? 0 : children.length);
+        int numFails = 0;
+        if (0 < numChildren) {
+            out.println(prefix + "--- printing children [" + numChildren + "]");
+            int i = 0;
+            for (int j = 0; j < children.length; j++) {
+                print(out, prefix + "[" + LangUtil.toSizedString(i++, 3) + "]: ", children[j]);
+                if (!RunValidator.NORMAL.runPassed(children[j])) {
+                    numFails++;
+                }
+            }
+        }
+        if (0 < numFails) {
+            label = numFails + " fails " + label;
+        }
+        out.println("");
+    }
+
+   
+    public static String debugString(IRunStatus status) {
+        if (null == status) {
+            return "null";
+        }
+        final String[] LABELS =
+            new String[] {
+                "runResult",
+                "id",
+                "result",
+                "numChildren",
+                "completed",
+                //"parent",
+                "abort",
+                "started",
+                "thrown",
+                "messages" };
+        String runResult = status.runResult() ? "PASS" : "FAIL";
+        Throwable thrown = status.getThrown();
+        String thrownString = LangUtil.unqualifiedClassName(thrown);
+        IRunStatus[] children = status.getChildren();
+        String numChildren = (null == children? "0" : ""+children.length);
+        String numMessages = ""+status.numMessages(null, IMessageHolder.EQUAL);
+        Object[] values =
+            new Object[] {
+                runResult,
+                status.getIdentifier(),
+                status.getResult(),
+                numChildren,
+                new Boolean(status.isCompleted()),
+                //status.getParent(),               // costly if parent printing us
+                status.getAbortRequest(),
+                new Boolean(status.started()),
+                thrownString,
+                numMessages };
+        return org.aspectj.testing.util.LangUtil.debugStr(status.getClass(), LABELS, values);
+    }
+
+    public static String toShortString(IRunStatus status) {
+        if (null == status) {
+            return "null";
+        }
+        String runResult = status.runResult() ? " PASS: " : " FAIL: ";
+        return (runResult + status.getIdentifier());
+    }
+    
+    /** renderer for IRunStatus */
+    public static interface IRunStatusPrinter {
+        void printRunStatus(PrintStream out, IRunStatus status);
+    }
+    
+    public static final IRunStatusPrinter VERBOSE_PRINTER = new IRunStatusPrinter() {
+        public String toString() { return "VERBOSE_PRINTER"; }
+        /** Render IRunStatus produced by running an AjcTest */
+        public void printRunStatus(PrintStream out, IRunStatus status) {
+            printRunStatus(out, status, "");
+        }
+        private void printRunStatus(PrintStream out, IRunStatus status, String prefix) {
+            LangUtil.throwIaxIfNull(out, "out");
+            LangUtil.throwIaxIfNull(status, "status");
+            String label = (status.runResult() ? " PASS: " : " FAIL: ") 
+                + status.getIdentifier();       
+            out.println(prefix + "------------ " + label);
+            out.println(prefix + "--- result: " + status.getResult());
+            if (0 < status.numMessages(null, true)) {
+                out.println(prefix + "--- messages ");
+                MessageUtil.print(out, status, prefix, MessageUtil.MESSAGE_ALL, MessageUtil.PICK_ALL);
+            }
+            Throwable thrown = status.getThrown();
+            if (null != thrown) {
+                out.println(prefix + "--- thrown");
+                thrown.printStackTrace(out);
+            } 
+            IRunStatus[] children = status.getChildren();
+            for (int i = 0; i < children.length; i++) {
+                String number = "[" + LangUtil.toSizedString(i,3) + "] ";
+                printRunStatus(out, children[i], prefix + number);
+                       }
+        }            
+    };
+
+    /** print only status and fail/abort messages */
+    public static final IRunStatusPrinter TERSE_PRINTER = new IRunStatusPrinter() {
+        public String toString() { return "TERSE_PRINTER"; }
+
+        /** print only status and fail messages */
+        public void printRunStatus(PrintStream out, IRunStatus status) {
+            printRunStatus(out, status, "");
+        }
+        private void printRunStatus(PrintStream out, IRunStatus status, String prefix) {
+            LangUtil.throwIaxIfNull(out, "out");
+            LangUtil.throwIaxIfNull(status, "status");
+            String label = (status.runResult() ? "PASS: " : "FAIL: ") 
+                + status.getIdentifier();       
+            out.println(prefix + label);
+            Object result = status.getResult();
+            if ((null != result) && (IRunStatus.PASS != result) && (IRunStatus.FAIL != result)) {
+                out.println(prefix + "--- result: " + status.getResult());
+            } 
+            if (0 < status.numMessages(IMessage.FAIL, true)) {
+                MessageUtil.print(out, status, prefix, MessageUtil.MESSAGE_ALL, MessageUtil.PICK_FAIL_PLUS);
+            }
+            Throwable thrown = status.getThrown();
+            if (null != thrown) {
+                out.println(prefix + "--- thrown: " + LangUtil.renderException(thrown, true));
+            } 
+            IRunStatus[] children = status.getChildren();
+            for (int i = 0; i < children.length; i++) {
+                if (!children[i].runResult()) {
+                    String number = "[" + LangUtil.toSizedString(i,3) + "] ";
+                    printRunStatus(out, children[i], prefix + number);
+                }
+            }
+            out.println("");
+        }
+    };
+
+    /** Render IRunStatus produced by running an AjcTest.Suite. */
+    public static final IRunStatusPrinter AJCSUITE_PRINTER  = new IRunStatusPrinter() {
+        public String toString() { return "AJCSUITE_PRINTER"; }
+        
+        /** 
+         * Render IRunStatus produced by running an AjcTest.Suite.
+         * This renders only test failures and 
+         * a summary at the end.
+         */
+        public void printRunStatus(PrintStream out, IRunStatus status) {
+            LangUtil.throwIaxIfNull(out, "out");
+            LangUtil.throwIaxIfNull(status, "status");
+            final String prefix = "";
+            final boolean failed = status.runResult();
+            String label = (status.runResult() ? "PASS: " : "FAIL: ") 
+                + status.getIdentifier();       
+            out.println(prefix + label);
+            // print all messages - these are validator comments
+            if (0 < status.numMessages(null, true)) {
+                MessageUtil.print(out, status, "init", MessageUtil.MESSAGE_ALL, MessageUtil.PICK_ALL);
+            }
+            // XXX ignore thrown if failed - will be printed as message anyway?
+            Throwable thrown = status.getThrown();
+            if ((null != thrown) && !failed) {
+                out.println(prefix + "--- printing stack trace for thrown");
+                thrown.printStackTrace(out);
+            } 
+            IRunStatus[] children = status.getChildren();
+            int numChildren = (null == children? 0 : children.length);
+            int numFails = 0;
+            if (0 < numChildren) {
+                for (int j = 0; j < children.length; j++) {
+                    if (!RunValidator.NORMAL.runPassed(children[j])) {
+                        numFails++;
+                    }
+                }
+            }
+            if (0 < numFails) {
+                out.println(prefix + "--- " + numFails + " failures when running " + children.length + " tests");
+                for (int j = 0; j < children.length; j++) {
+                    if (!RunValidator.NORMAL.runPassed(children[j])) {
+                        print(out, prefix + "[" + LangUtil.toSizedString(j, 3) + "]: ", children[j]);
+                        out.println("");
+                    }
+                }
+            }
+            label = "ran " + children.length + " tests" 
+                + (numFails == 0 ? "" : "(" + numFails + " fails)");            
+            out.println("");
+        }
+        
+    };
+    /** Render IRunStatus produced by running an AjcTest (verbose) */
+    public static final IRunStatusPrinter AJCTEST_PRINTER = VERBOSE_PRINTER;    
+
+       /** print this with messages, then children using AJCRUN_PRINTER */
+    public static final IRunStatusPrinter AJC_PRINTER = new IRunStatusPrinter() {
+        public String toString() { return "AJC_PRINTER"; }
+        /** Render IRunStatus produced by running an AjcTest */
+        public void printRunStatus(PrintStream out, IRunStatus status) {
+            LangUtil.throwIaxIfNull(out, "out");
+            LangUtil.throwIaxIfNull(status, "status");
+            String label = (status.runResult() ? " PASS: " : " FAIL: ") 
+                + status.getIdentifier();       
+            out.println("------------ " + label);
+            MessageUtil.print(out, status, "", MessageUtil.MESSAGE_ALL, MessageUtil.PICK_ALL);
+            IRunStatus[] children = status.getChildren();
+            for (int i = 0; i < children.length; i++) {
+                AJCRUN_PRINTER.printRunStatus(out, children[i]);
+                       }
+            //out.println("------------   END "  + label);
+            out.println("");
+        }        
+    };
+
+
+       /** print only fail messages */
+    public static final IRunStatusPrinter AJCRUN_PRINTER = new IRunStatusPrinter() {
+        public String toString() { return "AJCRUN_PRINTER"; }
+        /** Render IRunStatus produced by running an AjcTest child */
+        public void printRunStatus(PrintStream out, IRunStatus status) {
+            LangUtil.throwIaxIfNull(out, "out");
+            LangUtil.throwIaxIfNull(status, "status");
+            final boolean orGreater = false;
+            int numFails = status.numMessages(IMessage.FAIL, orGreater);
+            if (0 < numFails) { 
+                out.println("--- " + status.getIdentifier());
+                IMessage[] fails = status.getMessages(IMessage.FAIL, orGreater); 
+                for (int i = 0; i < fails.length; i++) {
+                    out.println("[fail " + LangUtil.toSizedString(i, 3) + "]: " 
+                      + MessageUtil.MESSAGE_ALL.renderToString(fails[i]));
+                               }
+            }
+        }        
+    };
+
+       private RunUtils() {
+       }
+    
+}
diff --git a/testing/src/org/aspectj/testing/util/SFileReader.java b/testing/src/org/aspectj/testing/util/SFileReader.java
new file mode 100644 (file)
index 0000000..f2b57e1
--- /dev/null
@@ -0,0 +1,185 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.util.LangUtil;
+import org.aspectj.util.LineReader;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+
+/**
+ * This reads a structured (config) file, which may have
+ * lines with @ signalling a recursive read
+ * and EOL comments # or //.
+ * This duplicates ConfigFileUtil in some sense.
+  */
+public class SFileReader { 
+    // XXX move into LineReader, but that forces util to depend on AbortException?
+    // Formerly in SuiteReader
+    
+    /** 
+     * Read args as config files and echo to stderr.
+     * @param args String[] of fully-qualified paths to config files
+     */
+    public static void main(String[] args) throws IOException {
+        ArrayList result = new ArrayList();
+        ObjectChecker collector = new StandardObjectChecker(String.class, result);
+        SFileReader me = new SFileReader(null);
+        for (int i = 0; i < args.length; i++) {
+                 Node node = me.readNodes(new File(args[i]), null, true, System.err);  
+            if (!Node.visit(node, collector, null)) {
+                System.err.println("halted during copy of " +args[i]);
+            } else {
+              String s = org.aspectj.testing.util.LangUtil.debugStr(null, "\n  ", null,
+                null, result.toArray(), "\n  ", "");
+              System.err.println(args[i] + ": " + s);
+            }
+               }
+    }
+    
+    /*
+     * readSuite(..) reads .txt file, and for each test case specification
+     * creates a spec using readTestSpecifications 
+     * and (if the specifications match the constraints)
+     * creates a test case using creatTestCase.
+     */
+
+    /** pass this to protected methods requiring String[] if you have none */
+    protected static final String[] NONE = new String[0];
+    
+    final Maker maker;
+    
+    /** @param maker the Maker factory to use - if null, use Maker.ECHO */
+    public SFileReader(Maker maker) {
+        this.maker = (null == maker ? Maker.ECHO : maker);
+    }
+
+    /**
+     * Creates a (potentially recursive) tree of node
+     * by reading from the file and constructing using the maker.
+     * Clients may read results in Node tree form when complete
+     * or snoop the selector for a list of objects made.
+     * The selector can prevent collection in the node by
+     * returning false.
+     * Results are guaranteed by the Maker to be of the Maker's type.
+     * @param file an absolute path to a structured file
+     * @param selector determines whether not to keep an object made.
+     *         (if null, then all are kept)
+     * @return Node with objects available from getItems()
+     *          and sub-suite Node available from getNodes()
+     * @throws Error on any read error if abortOnReadError (default)
+     */
+    public Node readNodes(
+        final File file,
+        final ObjectChecker selector,
+        final boolean abortOnReadError,
+        final PrintStream err)
+        throws IOException {
+        final Node result = new Node(file.getPath(), maker.getType());
+        if (null == file) {
+            throw new IllegalArgumentException("null file");
+        } else if (!file.isAbsolute()) {
+            throw new IllegalArgumentException("file not absolute");
+        }
+        LineReader reader = null;
+        try {
+            reader = LineReader.createTester(file);
+            if (null == reader) {
+                throw new IOException("no reader for " + file);
+            }
+            final String baseDir = file.getParent();
+    
+            String line;
+            boolean skipEmpties = true;
+            while (null != (line = reader.nextLine(skipEmpties))) {
+                if (line.charAt(0) == '@') {
+                    if (line.length() > 1) {
+                        String newFilePath = line.substring(1).trim();
+                        File newFile = new File(newFilePath);
+                        if (!newFile.isAbsolute()) {
+                            newFile = new File(baseDir, newFilePath);
+                        }
+                        Node node = readNodes(newFile, selector, abortOnReadError, err);
+                        if (!result.addNode(node)) {
+                            // XXX signal error?
+                            System.err.println("warning: unable to add node: " + node);
+                            break;
+                        }
+                    }
+                } else {
+                    try {
+                        Object made = maker.make(reader);
+                        if ((null == selector) || (selector.isValid(made))) {
+                            if (!result.add(made)) { 
+                               break;  // XXX signal error?
+                            }
+                        }
+                    } catch (AbortException e) {
+                        if (abortOnReadError) { // XXX todo - verify message has context?
+                           throw e;
+                        }
+                        if (null != err) {
+                            String m;
+                            IMessage mssg = e.getIMessage();
+                            if (null != mssg) {
+                                m = "Message: " + mssg;
+                            } else {
+                                m = LangUtil.unqualifiedClassName(e) + "@" + e.getMessage();
+                            }
+                            err.println(m);
+                        }
+                        reader.readToBlankLine();
+                    }
+                } 
+            }
+        } finally {
+            try {
+                if (null != reader) {
+                    reader.close();
+                }
+            } catch (IOException e) {
+            } // ignore
+        }
+        
+        return result;
+    }
+    
+    /** factory produces objects by reading LineReader */
+    public interface Maker {
+        /** 
+         * Make the result using the input from the LineReader,
+         * starting with lastLine().
+         */
+        Object make(LineReader reader) throws AbortException, IOException;
+        
+        /** @return type of the Object made */
+        Class getType();
+        
+        /** This echoes each line, prefixed by the reader.
+         * @return file:line: {line}
+         */
+        static final Maker ECHO = new Maker() {
+            public Object make(LineReader reader) {
+                return reader + ": " + reader.lastLine();
+            }
+            public Class getType() { return String.class; }
+        };
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/StandardObjectChecker.java b/testing/src/org/aspectj/testing/util/StandardObjectChecker.java
new file mode 100644 (file)
index 0000000..ee5a466
--- /dev/null
@@ -0,0 +1,127 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * StandardObjectChecker.java created on May 7, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+import java.util.List;
+
+/**
+ * Superclass for checkers that require non-null input
+ * of a given type.
+ * Clients may supply delegator for further checks,
+ * or a list to collect results.
+ * Subclasses may instead implement doIsValid().
+ * @author isberg
+ */
+public class StandardObjectChecker implements ObjectChecker {
+
+    public final Class type;
+    private final ObjectChecker delegate;
+    private final List collector;
+    private final boolean collectionResult;
+    
+    /**
+     * Create one with no delegate.
+     * @param type the Class of the type required of the input
+     */
+    public StandardObjectChecker(Class type) {
+        this(type, ANY, (List) null, true);
+    }
+    
+    /**
+     * @param type the Class of the type required of the input
+     * @param delegate the ObjectChecker to delegate to after
+     *         checking for non-null input of the correct type.
+     */
+    public StandardObjectChecker(Class type, ObjectChecker delegate) {
+        this(type, delegate, null, true);
+    }
+
+    /**
+     * same as StandardObjectChecker(type, collector, true)
+     * @param type the Class of the type required of the input
+     * @param collector the list to collect valid entries
+     */
+    public StandardObjectChecker(Class type, List collector) {
+        this(type, ANY, collector, true);
+    }
+    
+    /**
+     * @param type the Class of the type required of the input
+     * @param collector the list to collect valid entries
+     * @param collectionResult the value to return when entry was added
+     */
+    public StandardObjectChecker(Class type, List collector, boolean collectionResult) {
+        this(type, ANY, collector, collectionResult);
+    }
+    
+    /**
+     * @param type the Class of the type required of the input
+     * @param collector the list to collect valid entries
+     */
+    public StandardObjectChecker(Class type, ObjectChecker delegate, 
+                                List collector, boolean collectionResult) {
+        if (null == type) throw new IllegalArgumentException("null type");
+        this.type = type;
+        this.delegate = delegate;
+        this.collector = collector;
+        this.collectionResult = collectionResult;
+    }
+    
+    /**
+     * Check if object is valid by confirming is is non-null and of the
+     * right type, then delegating to any delegate or calling doIsValid(),
+     * then (if true) passing to any collector, and returning
+     * false if the collector failed or the collection result otherwise.
+     * @see ObjectChecker#isValid(Object)
+     * @return true unless input is null or wrong type 
+     *          or if one of subclass (doIsValid(..)) or delegates
+     *          (list, collector) returns false.
+     */
+    public final boolean isValid(Object input) {
+        if ((null == input) || (!(type.isAssignableFrom(input.getClass())))) {
+            return false;
+        } else if (null != delegate) {
+            if (!delegate.isValid(input)) {
+                return false;
+            }
+        } 
+        if (!doIsValid(input)) {
+            return false;
+        }
+        if (null == collector) {
+            return true;
+        } else if (!collector.add(input)) {
+            return false;
+        } else {
+            return collectionResult;
+        }
+    }
+    
+    /**
+     * Delegate of isValid guarantees that the input
+     * is not null as is assignable to the specified type.
+     * Subclasses implement their funtionality here.
+     * This implementation returns true;
+     * @return true
+     */
+    public boolean doIsValid(Object input) {
+        return true;
+    }
+
+}
diff --git a/testing/src/org/aspectj/testing/util/StreamSniffer.java b/testing/src/org/aspectj/testing/util/StreamSniffer.java
new file mode 100644 (file)
index 0000000..8b740aa
--- /dev/null
@@ -0,0 +1,76 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * StreamGrabber.java created on May 16, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+  * Listen to a stream using StringBuffer.
+  * Clients install and remove buffer to enable/disable listening.
+  * Does not affect data passed to underlying stream
+  */
+public class StreamSniffer extends FilterOutputStream {
+    StringBuffer buffer;
+    /** have to use delegate, not super, because super we will double-count input */
+    final OutputStream delegate;
+    
+    public StreamSniffer(OutputStream stream) {
+        super(stream);
+        delegate = stream;
+    }
+
+    /** set to null to stop copying */
+    public void setBuffer(StringBuffer sb) {
+        buffer = sb;
+    }
+
+    //---------------- FilterOutputStream 
+    public void write(int b) throws IOException {
+        StringBuffer sb = buffer;
+        if (null != sb) {
+            if ((b > Character.MAX_VALUE) 
+                || (b < Character.MIN_VALUE)) {
+                throw new Error("don't know double-byte"); // XXX
+            } else {
+                sb.append((char) b);
+            }
+        }
+        delegate.write(b);
+    }
+    
+    public void write(byte[] b) throws IOException {
+        StringBuffer sb = buffer;
+        if (null != sb) {
+            String s = new String(b);
+            sb.append(s);
+        }
+        delegate.write(b);
+    }
+    
+    public void write(byte[] b, int offset, int length) throws IOException {
+        StringBuffer sb = buffer;
+        if (null != sb) {
+            String s = new String(b, offset, length);
+            sb.append(s);
+        }
+        delegate.write(b, offset, length);
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/StreamsHandler.java b/testing/src/org/aspectj/testing/util/StreamsHandler.java
new file mode 100644 (file)
index 0000000..bd550ad
--- /dev/null
@@ -0,0 +1,217 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.io.PrintStream;
+
+/**
+ * Manage system err and system out streams.
+ * Clients can suppress stream output during StreamsHandler lifecycle
+ * and intermittantly listen to both streams if signalled on construction.
+ * To print to the underlying streams (without hiding or listening),
+ * use either the log methods (which manage lineation)
+ * or the out and err fields.
+ * <pre>
+ * boolean hideStreams = true;
+ * boolean listen = true;
+ * StreamsHander streams = new StreamsHander(hideStreams, listen);
+ * streams.startListening();
+ * ...
+ * streams.out.println("this goes out to without listening"); XXX verify
+ * StreamsHandler.Result result = streams.stopListening();
+ * streams.restoreStreams();
+ * System.out.println("Suppressed output stream follows");
+ * System.out.print(result.out);
+ * System.out.println("Suppressed error stream follows");
+ * System.out.print(result.err);
+ * </pre>
+ * Warning: does not distinguish streams from different threads.
+ */
+public class StreamsHandler {
+
+    /** real output stream and sink for log if logToOut */
+    public final PrintStream out;
+
+    /** real error stream and sink for log if !logToOut */
+    public final PrintStream err;
+
+    /** if true, then can listen using startListening() */
+    protected final boolean listening;
+
+    /** if logToOut, then out, else err */
+    private final PrintStream log;
+    
+    /** true if the last logged item was a newline */
+    private boolean loggedLine;
+
+    /** sniffs stream to gather test output to System.out */
+    protected StreamSniffer outSniffer;
+
+    /** sniffs stream to gather test output to System.err */
+    protected StreamSniffer errSniffer;
+
+    /** permits us to hide output stream (after sniffing by outSniffer */
+    protected ProxyPrintStream outDelegate;
+
+    /** permits us to hide error stream (after sniffing by errSniffer */
+    protected ProxyPrintStream errDelegate;
+
+    /** when sniffing, this has sniffed contents of output stream */
+    protected StringBuffer outListener;
+
+    /** when sniffing, this has sniffed contents of error stream */
+    protected StringBuffer errListener;
+
+    /** @param hide if true, then suppress stream output (can still listen) */
+    public StreamsHandler(boolean listen) {
+        this(listen, false);
+    }
+
+    /**
+     * @param listen possible to sniff streams only if true
+     * @param logToOut if true, then log methods go to System.out -- otherwise, System.err. 
+     */
+    public StreamsHandler(
+        boolean listen,
+        boolean logToOut) {
+        this.err = System.err;
+        this.out = System.out;
+        outDelegate = new ProxyPrintStream(System.out);
+        errDelegate = new ProxyPrintStream(System.err);
+        this.listening = listen;
+        final PrintStream HIDE = NullPrintStream.NULL_PrintStream;
+        outSniffer = new StreamSniffer(outDelegate);
+        System.setOut(new PrintStream(outSniffer));
+        errSniffer = new StreamSniffer(errDelegate);
+        System.setErr(new PrintStream(errSniffer));
+        log = (logToOut ? this.out : this.err);
+        loggedLine = true;
+    }
+
+    /** render output and error streams (after sniffing) */
+    public void show() {
+        outDelegate.show();
+        errDelegate.show();
+    }
+
+    /** suppress output and error streams (after sniffing) */
+    public void hide() {
+        outDelegate.hide();
+        errDelegate.hide();
+    }
+
+    /** restore streams.  Do not use this after restoring. */
+    public void restoreStreams() {
+        if (null != outSniffer) {
+            outSniffer = null;
+            errSniffer = null;
+            System.setOut(out);
+            System.setErr(err);
+        }
+    }
+
+    /** @return PrintStream used for direct logging */
+    public PrintStream getLogStream() {
+        return log;
+    }
+    
+    /** log item without newline. */
+    public void log(String s) {
+        log.print(s);
+        if (loggedLine) {
+            loggedLine = false;
+        }
+    }
+
+    /** 
+     * Log item with newline.
+     * If previous log did not have a newline, 
+     * then this prepends a newline.
+     */
+    public void lnlog(String s) {
+        if (!loggedLine) {
+            log.println("");
+        }
+        log.println(s);
+    }
+
+    /** 
+     * Start listening to both streams.
+     * Tosses any old data captured.
+     * (Has no effect if not listening.)
+     * @throws IllegalStateException if called after restoreStreams()
+     * @see endListening()
+     */
+    public void startListening() {
+        if (null == outSniffer) {
+            throw new IllegalStateException("no listening after restore");
+        }
+        if (listening) {
+            if (null != outListener) {
+                outListener.setLength(0);
+                errListener.setLength(0);
+            } else {
+                outListener = new StringBuffer();
+                outSniffer.setBuffer(outListener);
+                errListener = new StringBuffer();
+                errSniffer.setBuffer(errListener);
+            }
+        }
+    }
+
+    /** 
+     * End listening to both streams and return data captured.
+     * Must call startListening() first.
+     * @throws IllegalStateException if called when not listening
+     * @return Result with sniffed output and error String
+     * @see startListening()
+     */
+    public Result endListening() {
+        return endListening(true);
+    }
+
+    /** 
+     * End listening to both streams and return data captured.
+     * Must call startListening() first.
+     * @param getResult if false, return Result.EMPTY 
+     *         and avoid converting buffer to String.
+     * @throws IllegalStateException if called when not listening
+     * @return Result with sniffed output and error String
+     * @see startListening()
+     */
+    public Result endListening(boolean getResult) {
+        if (!listening) {
+            return Result.EMPTY;
+        }
+        if (null == outListener) {
+            throw new IllegalStateException("listening not started");
+        }
+        Result result = (!getResult ? Result.EMPTY
+                : new Result(outListener.toString(), errListener.toString()));
+        errListener = null;
+        outListener = null;
+        return result;        
+    }
+
+    /** output and error String */
+    public static class Result {
+        static final Result EMPTY = new Result(null, null);
+        public final String out;
+        public final String err;
+        private Result(String out, String err) {
+            this.out = out;
+            this.err = err;
+        }
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/StringAccumulator.java b/testing/src/org/aspectj/testing/util/StringAccumulator.java
new file mode 100644 (file)
index 0000000..ecff4fa
--- /dev/null
@@ -0,0 +1,99 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * StringAccumulator.java created on May 14, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+
+
+/**
+ * Accumulate String with delimiters.
+ */
+public class StringAccumulator implements ObjectChecker {
+
+    private final String prefix;
+    private final String infix;
+    private final String suffix;
+    private final String nullString;
+    private final StringBuffer sb;
+    private int index;
+
+    /**
+     * Accumulate string with delimiter between elements,
+     * treaing null elements as "".
+     */
+    public StringAccumulator(String delimiter) {
+        this(delimiter, null, null, "");
+    }
+    
+    /**
+     * Constructor for StringAccumulator which specifies how to 
+     * process each result, optionally postfixing or prefixing
+     * or infixing (adding index plus infix to prefix). e.g.,
+     * for prefix="[", infix="]\"", postfix="\"\n", then each entry
+     * becomes a line: <pre>"[{index}]"{entry}"\n</pre>
+     * 
+     * @param prefix if not null, prepend to each result
+     * @param infix if not null, the add index and infix before each result, after prefix
+     * @param postfix if not null, append to each result
+     * @param nullString if null, ignore null completely (no index); otherwise render null as nullString
+     * @param type
+     */
+    public StringAccumulator(String prefix, String infix, String suffix, String nullString) {
+        this.prefix = prefix;
+        this.infix = infix;
+        this.suffix = suffix;
+        this.nullString = nullString;
+        sb = new StringBuffer();
+    }
+    
+    /** Clear buffer and index */
+    public synchronized void clear() { 
+        sb.setLength(0); 
+        index = 0;
+    }
+
+    /**
+     * Accumulate input.toString into
+     * @return true
+     * @see StandardObjectChecker#doIsValid(Object)
+     */
+    public synchronized boolean isValid(Object input) {
+        if (input == null) {
+            if (nullString == null) return true; // ignore
+            input = nullString;
+        }
+        if (null != prefix) sb.append(prefix);
+        if (null != infix) {
+            sb.append(index++ + infix);
+        }
+        sb.append(input.toString());
+        if (null != suffix) sb.append(suffix);
+        return true;
+    }
+    
+    /** @return result accumulated so far */
+    public String toString() {
+        return sb.toString();
+    }
+    /** @return result accumulated so far */
+    public String debugString() {
+        return "StringAccumulator prefix=" + prefix + " infix=" + infix + " suffix=" + suffix
+                + " nullString=" + nullString + " index=" + index + " toString=" + toString();
+    }
+
+}
diff --git a/testing/src/org/aspectj/testing/util/StringVisitor.java b/testing/src/org/aspectj/testing/util/StringVisitor.java
new file mode 100644 (file)
index 0000000..3d14cb3
--- /dev/null
@@ -0,0 +1,28 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+// todo: non-distribution license?
+
+package org.aspectj.testing.util;
+
+/** 
+ * Visitor interface for String
+*/
+public interface StringVisitor {
+    /**
+     * @param input the String to evaluate - may be null
+     * @return true if input is accepted and/or process should continue
+     */
+    public boolean accept(String input);
+}
+
diff --git a/testing/src/org/aspectj/testing/util/TestClassLoader.java b/testing/src/org/aspectj/testing/util/TestClassLoader.java
new file mode 100644 (file)
index 0000000..2af325e
--- /dev/null
@@ -0,0 +1,154 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Load classes as File from File[] dirs or URL[] jars.
+ */
+public class TestClassLoader extends URLClassLoader {
+
+    List /*File*/ dirs;
+    
+    public TestClassLoader(URL[] urls, File[] dirs) {
+        super(urls);
+        if (null == dirs) {
+            throw new IllegalArgumentException("null dir");
+        }
+        for (int i = 0; i < dirs.length; i++) {
+            if (null == dirs[i]) {
+                throw new IllegalArgumentException("null dir[" + i + "]");
+            }
+               }
+        ArrayList dcopy = new ArrayList();
+        
+        if ((null != dirs) && (0 < dirs.length)) {
+            dcopy.addAll(Arrays.asList(dirs));
+        }
+        this.dirs = Collections.unmodifiableList(dcopy);
+    }
+
+    
+    public URL getResource(String name) {
+        return ClassLoader.getSystemResource(name);
+    }
+    
+    public InputStream getResourceAsStream(String name) {
+        return ClassLoader.getSystemResourceAsStream(name);
+    } 
+    
+    /** We don't expect test classes to have prefixes java, org, or com */
+    protected boolean maybeTestClassName(String name) {
+        return (null != name)
+            && !name.startsWith("java")
+            && !name.startsWith("org.")
+            && !name.startsWith("com.");
+    }
+    
+    public synchronized Class loadClass(String name, boolean resolve)
+        throws ClassNotFoundException {
+        // search the cache, our dirs (if maybe test), 
+        // the system, the superclass (URL[]),
+        // and our dirs again (if not maybe test)
+        ClassNotFoundException thrown = null;
+        final boolean maybeTestClass = maybeTestClassName(name);
+            
+        Class result =  findLoadedClass(name);
+        if (null != result) {
+            resolve = false;
+        } else if (maybeTestClass) {
+            // subvert the dominant paradigm...
+            byte[] data = readClass(name);
+            if (data != null) {
+                result = defineClass(name, data, 0, data.length);
+            } // handle ClassFormatError?
+        }
+        if (null == result) {
+            try { 
+                result = findSystemClass(name); 
+            } catch (ClassNotFoundException e) { 
+                thrown = e; 
+            }
+        }
+        if (null == result) {
+            try {
+                result = super.loadClass(name, resolve);
+            } catch (ClassNotFoundException e) {
+                thrown = e;
+            }
+            if (null != result) { // resolved by superclass
+                return result; 
+            }
+        }
+        if ((null == result) && !maybeTestClass) {
+            byte[] data = readClass(name);
+            if (data != null) {
+                result = defineClass(name, data, 0, data.length);
+            } // handle ClassFormatError?            
+        }
+        
+        if (null == result) {
+            throw (null != thrown ? thrown : new ClassNotFoundException(name));
+        }
+        if (resolve) {
+            resolveClass(result);
+        }
+        return result;
+    }
+    
+    private byte[] readClass(String className) throws ClassNotFoundException {
+        byte[] data= null;
+        final String fileName = className.replace('.', '/')+".class";
+        for (Iterator iter = dirs.iterator(); iter.hasNext();) {
+            File file = new File((File) iter.next(), fileName);
+            if (file.canRead()) { 
+                return getClassData(file);
+            }
+            if (data != null) {
+                return data;
+            }
+        }
+        throw new ClassNotFoundException(className); // expensive - fix?
+    }
+        
+    private byte[] getClassData(File f) {
+        try {
+            FileInputStream stream= new FileInputStream(f);
+            ByteArrayOutputStream out= new ByteArrayOutputStream(1000);
+            byte[] b= new byte[4096];
+            int n;
+            while ((n= stream.read(b)) != -1) {
+                out.write(b, 0, n);
+            }
+            stream.close();
+            out.close();
+            return out.toByteArray();
+        } catch (IOException e) {
+        }
+        return null;
+    }
+}
+
diff --git a/testing/src/org/aspectj/testing/util/TestDiffs.java b/testing/src/org/aspectj/testing/util/TestDiffs.java
new file mode 100644 (file)
index 0000000..2a847f0
--- /dev/null
@@ -0,0 +1,360 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.util.LangUtil;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Calculated differences between two test runs 
+ * based on their output files
+ * assuming that tests are logged with prefix [PASS|FAIL]
+ * (as they are when using <tt>-traceTestsMin</tt> with the Harness).
+ * @see org.aspectj.testing.drivers.Harness
+ */
+public class TestDiffs { // XXX pretty dumb implementation
+
+    /** @param args expected, actual test result files */
+    public static void main(String[] args) {
+        if ((null == args) || (2 > args.length)) {
+            System.err.println("java " + TestDiffs.class.getName() + " expectedFile actualFile {test}");
+            return;
+        }
+        File expected  = new File(args[0]);
+        File actual = new File(args[1]);
+
+        TestDiffs result = compareResults(expected, actual);
+
+        System.out.println("## Differences between test runs");
+        print(System.out, result.added, "added");
+        print(System.out, result.missing, "missing");
+        print(System.out, result.fixed, "fixed");
+        print(System.out, result.broken, "broken");
+        
+        System.out.println("## Summary");
+        System.out.println(" # expected " + result.expected.size() + " tests: " + args[0] );
+        System.out.println(" #   actual " +   result.actual.size() + " tests: " + args[1]);
+        StringBuffer sb = new StringBuffer();
+        append(sb, result.added, " added");
+        append(sb, result.missing, " missing");
+        append(sb, result.broken, " broken");
+        append(sb, result.fixed, " fixed");
+        append(sb, result.stillPassing, " still passing");
+        append(sb, result.stillFailing, " still failing");
+        System.out.println(" #    diffs: " + sb);
+    }
+        
+    /**
+     * @param expected the expected/old File results with Harness -traceTestsMin lines
+     * @param actual the actual/new File results with Harness -traceTestsMin lines
+     * @return TestDiffs null if error, valid otherwise
+     */
+    public static TestDiffs compareResults(File expected, File actual) {
+        ArrayList exp = null;
+        ArrayList act = null;
+        File reading = expected;
+        try {
+            exp = TestDiffs.readTestResults(expected, expected.getPath());
+            reading = actual;
+            act = TestDiffs.readTestResults(actual, actual.getPath());
+            
+            Diffs tests = new Diffs("tests", exp, act, TestResult.BY_NAME);
+            // remove missing/unexpected (removed, added) tests from results
+            // otherwise, unexpected-[pass|fail] look like [fixes|broken]
+            ArrayList expResults = trimByName(exp, tests.missing);
+            ArrayList actResults = trimByName(act, tests.unexpected);
+            
+            Diffs results = new Diffs("results", expResults, actResults, TestResult.BY_PASSNAME);
+
+            // broken tests show up in results as unexpected-fail or missing-pass
+            //  fixed tests show up in results as unexpected-pass or missing-fail
+            ArrayList broken = new ArrayList();
+            ArrayList fixed = new ArrayList();
+            split(results.unexpected, fixed, broken);
+
+            return new TestDiffs(
+                exp,
+                act,
+                tests.missing, 
+                tests.unexpected, 
+                broken, 
+                fixed);             
+        } catch (IOException e) {
+            System.err.println("error reading " + reading);
+            e.printStackTrace(System.err); // XXX
+            return null;
+        }
+    }
+
+    private static void append(StringBuffer sb, List list, String label) {
+        if (!LangUtil.isEmpty(list)) {
+            if (0 < sb.length()) {
+                sb.append(" ");
+            }
+            sb.append(list.size() + label);
+        }
+    }
+    
+    private static void print(PrintStream out, List list, String label) {
+        if ((null == out) || LangUtil.isEmpty(list)) {
+            return;
+        }
+        int i = 0;
+        final String suffix = " " + label;
+        final String LABEL = list.size() + suffix;
+        out.println("## START " + LABEL);
+        for (Iterator iter = list.iterator(); iter.hasNext();) {
+                       TestResult result = (TestResult) iter.next();
+                       out.println(" " + result.test + " ## " + suffix);
+               }
+        out.println("##   END " + LABEL);
+    }
+    
+    /**
+        * Create ArrayList with input TestResult list 
+     * but without elements in trim list,
+     * comparing based on test name only.
+        * @param input
+        * @param trim
+        * @return ArrayList with all input except those in trim (by name)
+        */
+       private static ArrayList trimByName(List input, List trim) {
+               ArrayList result = new ArrayList();
+        result.addAll(input);
+        if (!LangUtil.isEmpty(input) && !LangUtil.isEmpty(trim)) {
+            for (ListIterator iter = result.listIterator(); iter.hasNext();) {
+                               TestResult inputItem = (TestResult) iter.next();
+                for (Iterator iterator = trim.iterator();
+                                       iterator.hasNext();
+                                       ) {
+                                       TestResult trimItem = (TestResult) iterator.next();
+                                       if (inputItem.test.equals(trimItem.test)) {
+                        iter.remove();
+                        break;
+                    }
+                               }
+                       }
+        }
+        return result;
+       }
+
+    
+    /** split input List by whether the TestResult element passed or failed */
+    private static void split(List input, ArrayList pass, ArrayList fail) {
+        for (ListIterator iter = input.listIterator(); iter.hasNext();) {
+                       TestResult result = (TestResult) iter.next();
+            if (result.pass) {
+                pass.add(result);
+            } else {
+                fail.add(result);
+            }
+               }
+    }
+
+    /** 
+     * Read a file of test results,
+     * defined as lines starting with [PASS|FAIL]
+     * (produced by Harness option <tt>-traceTestsmin</tt>).
+     * @return ArrayList of TestResult, one for every -traceTestsMin line in File 
+     */
+    private static ArrayList readTestResults(File file, String config) throws IOException {
+        LangUtil.throwIaxIfNull(file, "file");
+        if (null == config) {
+            config = file.getPath();
+        }
+        ArrayList result = new ArrayList();
+        FileReader in = null;
+        try {
+            in = new FileReader(file);
+            BufferedReader input = new BufferedReader(in);
+            String line;
+            // XXX handle stream interleaving more carefully
+            // XXX clip trailing ()
+            // XXX fix elision in test name rendering by -traceTestsMin?
+            while (null != (line = input.readLine())) {
+                boolean pass = line.startsWith("PASS");
+                boolean fail = false;
+                if (!pass) {
+                    fail = line.startsWith("FAIL");
+                }
+                if (pass || fail) {
+                    String test = line.substring(4).trim();
+                    result.add(new TestResult(test, config, pass));
+                }
+            }
+        } finally {
+            if (null != in) {
+                try { in.close(); }
+                catch (IOException e) {} // ignore
+            }
+        }
+        return result;
+    }
+    
+    private static List safeList(List list) {
+        return (null == list
+            ? Collections.EMPTY_LIST
+            : Collections.unmodifiableList(list));
+    }
+    
+    /** List of TestResult results from expected run. */
+    public final List expected;
+
+    /** List of TestResult results from actual run. */
+    public final List actual;
+    
+    /** List of TestResult tests disappeared from test suite between expected and actual runs. */
+    public final List missing;
+    
+    /** List of TestResult tests added to test suite between expected and actual runs. */
+    public final List added;
+    
+    /** List of TestResult tests in both runs, expected to pass but actually failed */
+    public final List broken;
+    
+    /** List of TestResult tests in both runs, expected to fail but actually passed */
+    public final List fixed;
+    
+    /** List of TestResult passed tests in expected run */
+    public final List expectedPassed;
+
+    /** List of TestResult failed tests in expected run */
+    public final List expectedFailed;
+
+    /** List of TestResult passed tests in actual run */
+    public final List actualPassed;
+
+    /** List of TestResult tests failed in actual run */
+    public final List actualFailed;
+
+    /** List of TestResult tests passed in both expected and actual run */
+    public final List stillPassing;
+
+    /** List of TestResult tests failed in both expected and actual run */
+    public final List stillFailing;
+    
+    private TestDiffs(
+        List expected,
+        List actual,
+        List missing,
+        List added,
+        List broken,
+        List fixed) {
+        this.expected = safeList(expected);
+        this.actual = safeList(actual);
+        this.missing = safeList(missing);
+        this.added = safeList(added);
+        this.broken = safeList(broken);
+        this.fixed = safeList(fixed);
+        // expected[Passed|Failed]
+        ArrayList passed = new ArrayList();
+        ArrayList failed = new ArrayList();
+        split(this.expected, passed, failed);
+        expectedPassed = safeList(passed);
+        expectedFailed = safeList(failed);
+
+        // actual[Passed|Failed]
+        passed = new ArrayList();
+        failed = new ArrayList();
+        split(this.actual, passed, failed);
+        actualPassed = safeList(passed);
+        actualFailed = safeList(failed);
+
+        // stillPassing: expected.passed w/o broken, missingPasses
+        passed = new ArrayList();
+        passed.addAll(expectedPassed);
+        passed = trimByName(passed, this.broken);
+        ArrayList missingPasses = new ArrayList();
+        ArrayList missingFails = new ArrayList();
+        split(this.missing, missingPasses, missingFails);
+        passed = trimByName(passed, missingPasses); 
+        stillPassing = safeList(passed);
+
+        // stillFailing: expected.failed w/o fixed, missingFails
+        failed = new ArrayList();
+        failed.addAll(expectedFailed);
+        failed = trimByName(failed, this.fixed);
+        failed = trimByName(failed, missingFails); 
+        stillFailing = safeList(failed);
+    }
+       
+    /** results of a test */
+    public static class TestResult {
+        public static final Comparator BY_PASSNAME = new Comparator() {
+            public int compare(Object o1, Object o2) {
+                if (o1 == o2) {
+                    return 0;
+                }
+                TestResult lhs = (TestResult) o1;
+                TestResult rhs = (TestResult) o2;
+                return (lhs.pass == rhs.pass 
+                    ? lhs.test.compareTo(rhs.test)
+                    : (lhs.pass ? 1 : -1 ));
+            }
+            
+            public boolean equals(Object lhs, Object rhs) {
+                return (0 == compare(lhs, rhs));
+            }
+        };
+    
+        public static final Comparator BY_NAME = new Comparator() {
+            public int compare(Object o1, Object o2) {
+                if (o1 == o2) {
+                    return 0;
+                }
+                TestResult lhs = (TestResult) o1;
+                TestResult rhs = (TestResult) o2;
+                return lhs.test.compareTo(rhs.test);
+            }
+            
+            public boolean equals(Object lhs, Object rhs) {
+                return (0 == compare(lhs, rhs));
+            }
+        };
+        
+        //private static final ArrayList TESTS = new ArrayList();
+        public static final String FIELDSEP = "\t";
+                
+        public final String test;
+        public final String config;
+        public final boolean pass;
+        private final String toString;
+        
+        public TestResult(String test, String config, boolean pass) {
+            LangUtil.throwIaxIfNull(test, "test");
+            LangUtil.throwIaxIfNull(test, "config");
+            this.test = test;
+            this.config = config;
+            this.pass = pass;
+            toString = (pass ? "PASS" : "FAIL") + FIELDSEP + test + FIELDSEP + config;
+
+        }
+        
+        /** @return [PASS|FAIL]{FIELDSEP}test{FIELDSEP}config */
+        public String toString() {
+            return toString;
+        }
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/ValidFileFilter.java b/testing/src/org/aspectj/testing/util/ValidFileFilter.java
new file mode 100644 (file)
index 0000000..ba9f744
--- /dev/null
@@ -0,0 +1,123 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.io.FileFilter;
+
+/**
+ * FileFilter that accepts existing files
+ * with static singleton variants 
+ * made from inner subclasses.
+ */
+public class ValidFileFilter implements FileFilter {
+    //----------------------------- singleton variants
+    public static final FileFilter EXIST = new ValidFileFilter();
+    public static final FileFilter FILE_EXISTS = new FilesOnlyFilter();
+    public static final FileFilter DIR_EXISTS = new DirsOnlyFilter();
+    public static final FileFilter CLASS_FILE = new ClassOnlyFilter();
+    public static final FileFilter JAVA_FILE = new JavaOnlyFilter();
+    public static final FileFilter RESOURCE = new ResourcesOnlyFilter();
+
+    //----------------------------- members
+    protected final FileFilter delegate;
+    protected ValidFileFilter(){ 
+        this(null);
+    }
+    protected ValidFileFilter(FileFilter delegate){
+        this.delegate = delegate;
+    }
+
+    /**
+     * Implement <code>FileFilter.accept(File)</code> by checking 
+     * taht input is not null, exists, and is accepted by any delegate.
+     */
+    public boolean accept(File f) {
+        return ((null != f) && (f.exists())
+                && ((null == delegate) || delegate.accept(f)));
+    }
+
+    //----------------------------- inner subclasses
+    static class FilesOnlyFilter extends ValidFileFilter {
+        public boolean accept(File f) {
+            return (super.accept(f) && (!f.isDirectory()));
+        }
+    }
+    static class ResourcesOnlyFilter extends FilesOnlyFilter {
+        public boolean accept(File f) {
+            return (super.accept(f) && (FileUtil.isResourcePath(f.getPath())));
+        }
+    }
+    static class DirsOnlyFilter extends ValidFileFilter {
+        public final boolean accept(File f) {
+            return (super.accept(f) && (f.isDirectory()));
+        }
+    }
+    // todo: StringsFileFilter, accepts String[] variants for each
+    static class StringFileFilter extends ValidFileFilter {
+        public static final boolean IGNORE_CASE = true;
+        protected final String prefix;
+        protected final String substring;
+        protected final String suffix;
+        protected final boolean ignoreCase;
+        /** true if one of the String specifiers is not null */
+        protected final boolean haveSpecifier;
+        public StringFileFilter(String prefix, String substring, 
+                                String suffix, boolean ignoreCase) {
+            this.ignoreCase = ignoreCase;
+            this.prefix     = preprocess(prefix);
+            this.substring  = preprocess(substring);
+            this.suffix     = preprocess(suffix);
+            haveSpecifier   = ((null != prefix) || (null != substring) 
+                               || (null != suffix));
+        }
+        private final String preprocess(String input) {
+            if ((null != input) && ignoreCase) {
+                input = input.toLowerCase();
+            }
+            return input;
+        }
+        public boolean accept(File f) {
+            if (!(super.accept(f))) {
+                return false;
+            } else if (haveSpecifier) {
+                String path = preprocess(f.getPath());
+                if ((null == path) || (0 == path.length())) {
+                    return false;
+                }
+                if ((null != prefix) && (!(path.startsWith(prefix)))) {
+                    return false;
+                }
+                if ((null != substring) && (-1 == path.indexOf(substring))) {
+                    return false;
+                }
+                if ((null != suffix) && (!(path.endsWith(suffix)))) {
+                    return false;
+                }
+            } 
+            return true;
+        }
+    } // class StringFileFilter
+
+    static class ClassOnlyFilter extends StringFileFilter {
+        ClassOnlyFilter() {
+            super(null, null, ".class", IGNORE_CASE);
+        }
+    }
+    static class JavaOnlyFilter extends StringFileFilter {
+        JavaOnlyFilter() {
+            super(null, null, ".java", IGNORE_CASE);
+        }
+    }
+} // class ValidFileFilter
+
diff --git a/testing/src/org/aspectj/testing/util/WebInstall.java b/testing/src/org/aspectj/testing/util/WebInstall.java
new file mode 100644 (file)
index 0000000..4f8c969
--- /dev/null
@@ -0,0 +1,208 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2000 Xerox Corporation. 
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+/** 
+ * Install programmatically using http URL.
+ * (Very strange that java tool classpath does not accept http URL's.)
+ *
+ * Example:
+ * <p><code>java -classpath aj-testing.jar org.aspectj.testing.util.WebInstall
+ *    http://aspectj.org/download/distribution/aspectj10-tools.jar -text
+ *    install.properties</code>
+ *
+ * <p>You can omit the <code>-text install.properties</code> if there is
+ * a file called "install.properties" in the current directory.
+ *
+ * <p>The properties file must define the properties 
+ * <code>output.dir</code> and <code>context.javaPath</code>
+ * in properties- and platform specifie ways.  
+ * For property values, a backslash must be escaped with another backslash,
+ * and directory separators should be valid. E.g., on Windows:
+ * <pre>output.dir=c:\\output\\dir
+ * context.javaPath=c:\\apps\\jdk1.3.1</pre>
+ *
+ * For an installer to complete programmatically,
+ * the output directory must be empty of colliding files.
+ * This will fail with a stack trace if anything goes wrong, except for
+ * simple input errors.  
+ *
+ * <p>You may also use this as a driver for the known installers
+ * by specifying the following options (-baseurl must be first):<pre>
+ *         -baseurl           {baseurl}
+ *         -version           {version}
+ *         -context.javaPath  {path to JDK}    (properties form)
+ *         -output.dir        {path to outDir} (properties form, including trailing /)
+ *         -outdir            {path to outDir} (actual form) </pre>
+ * such that URL=
+ * <code>{baseurl}<packagePrefix>{version}<packageSuffix>.jar</code>
+ * and paths to context.javaPath and output.dir are specified in 
+ * properties-compliant format
+ *
+ * @see ant script test-product.xml for example of installing from files
+ *      which can be driven from the command-line.
+ */
+public class WebInstall {
+    private static final String EOL = "\n"; // todo where is this defined?
+    public static final String SYNTAX 
+        = "java WebInstall url {args}" + EOL
+        + "  url  - to installer"  + EOL
+        + "  args - normally -text install.properties"  + EOL
+        + "         (if n/a, use install.properties)"  + EOL;
+
+    /** default arguments assume file <code>install.properties</code> 
+     * is in current directory */
+    private static final String[] ARGS = new String[] 
+    { "-text", "install.properties" };
+
+    /** @param args the String[] <code>{ "<url>" {, "-text", "<propsPath>" }</code> */
+    public static void main(String[] args) throws Exception {
+        if ((null != args) && (args.length > 0) 
+            && ("-baseurl".equals(args[0]))) {
+            driver(args);
+        } else {
+            try {
+                new WebInstall().install(args);
+            } catch (Throwable t) {
+                System.err.println("Error installing args ");
+                for (int i = 0; i < args.length; i++) {
+                    System.err.println(" " + i + ": " + args[i]);
+                } 
+                t.printStackTrace(System.err);
+            }
+        }
+    }
+
+    /** known .jar packages {(prefix, suffix}...} */
+    protected static String[] packages = new String[] 
+    { "aspectj-tools-", ""
+    , "aspectj-docs-", ""
+    , "ajde-forteModule-", ""
+    , "ajde-jbuilderOpenTool-", ""
+    };
+
+    /**
+     * Drive install of all jar-based installers.
+     * @param args the String[] containing<pre>
+     *         -baseurl           {baseurl}
+     *         -version           {version}
+     *         -context.javaPath  {path to JDK}    (properties form)
+     *         -output.dir        {path to outDir} (properties form, including trailing /)
+     *         -outdir            {path to outDir} (actual form) </pre>
+     * such that URL=
+     * <code>{baseurl}<packagePrefix>{version}<packageSuffix>.jar</code>
+     * and paths to context.javaPath and output.dir are specified in 
+     * properties-compliant format
+     */
+    protected static void driver(String[] args) throws Exception {
+        String baseurl = null;
+        String version = null;
+        String outputDir = null;
+        File outdir = null;
+        String jdk = null;
+        for (int i = 0; i < args.length; i++) {
+            if ("-baseurl".equals(args[i])) {
+                baseurl = args[++i];
+            } else if ("-version".equals(args[i])) {
+                version = args[++i];
+            } else if ("-context.javaPath".equals(args[i])) {
+                jdk = args[++i];
+            } else if ("-output.dir".equals(args[i])) {
+                outputDir=args[++i];
+            } else if ("-outdir".equals(args[i])) {
+                outdir = new File(args[++i]).getCanonicalFile();
+                if (!outdir.isDirectory()) {
+                    outdir.mkdir();
+                }
+            }
+        }
+        final File props = File.createTempFile("WebInstall", null);
+        final String[] ARGS = new String [] {null, "-text", props.getCanonicalPath()};
+        for (int i = 0; i < packages.length; i++) {
+            String name = packages[i++] + version + packages[i];
+            File outDir = new File(outdir, name);
+            FileWriter fw = null;
+            try {
+                if (!outDir.isDirectory()) {
+                    outDir.mkdir();
+                }
+                fw = new FileWriter(props);
+                fw.write("output.dir=" + outputDir + name + "\n");
+                fw.write("context.javaPath=" + jdk + "\n");
+                fw.close(); 
+                fw = null;
+                ARGS[0] = baseurl + name + ".jar";
+                main(ARGS);
+            } finally {
+                try { if (null != fw) fw.close(); } 
+                catch (java.io.IOException e) {} // ignore
+            }
+        }
+        if (props.exists()) props.delete();
+    } // driver
+
+    private static boolean printError(String err) {
+        if (null != err) System.err.println(err);
+        System.err.println(SYNTAX);
+        return (null != err);
+    }
+
+    /**
+     * Create a classloader using the first argument (presumed to be URL for classloader),
+     * construct the installer, and invoke it using remaining arguments (or default args).
+     */
+    protected void install(String[] args) throws Exception {
+        if ((null == args) || (args.length < 1) 
+            || (null == args[0]) || (1 > args[0].length())) {
+            if (printError("expecting installer URL")) return;
+        }
+        URL[] urls = new URL[] { new URL(args[0]) };
+        //System.err.println("before: " + render(args));
+        args = getArgs(args);
+        //System.err.println("after: " + render(args));
+        URLClassLoader cl = new URLClassLoader(urls);
+        Class c = cl.loadClass("$installer$.org.aspectj.Main"); // todo: dependency on class name
+        Method ms = c.getMethod("main", new Class[]{String[].class});
+        ms.invoke(null, new Object[] { args });
+    }
+    public static final String render(String[] args) {
+        StringBuffer sb = new StringBuffer();
+        sb.append("[");
+        for (int i = 0; i < args.length; i++) {
+            if (0 < i) sb.append(", ");
+            sb.append("" + args[i]);
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /** @return args less args[0] or default args if less than 3 arguments */
+
+    protected String[] getArgs(String[] args) {
+        if ((null == args) || (args.length < 3)) {
+            return ARGS;
+        } else {
+            String[] result = new String[args.length-1];
+            System.arraycopy(args, 1, result, 0, result.length);
+            return result;
+        }
+    }
+
+}
diff --git a/testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java b/testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java
new file mode 100644 (file)
index 0000000..cc484f8
--- /dev/null
@@ -0,0 +1,432 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.xml;
+
+import org.apache.commons.digester.Digester;
+import org.aspectj.bridge.AbortException;
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.harness.bridge.AbstractRunSpec;
+import org.aspectj.testing.harness.bridge.AjcTest;
+import org.aspectj.testing.harness.bridge.CompilerRun;
+import org.aspectj.testing.harness.bridge.DirChanges;
+import org.aspectj.testing.harness.bridge.IncCompilerRun;
+import org.aspectj.testing.harness.bridge.JavaRun;
+import org.aspectj.testing.util.RunUtils;
+import org.aspectj.util.LangUtil;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/** 
+ * Read an ajc test specification in xml form. 
+ * Input files should comply with DOCTYPE
+ */
+public class AjcSpecXmlReader {
+    /*
+     * To add new elements or attributes:
+     * - update the DOCTYPE
+     * - update setupDigester(..)
+     *   - new sub-elements should be created 
+     *   - new attributes should have values set as bean properties
+     *     (possibly by mapping names)
+     *   - new sub-elements should be added to parents
+     *     => the parents need the add method - e.g., add{foo}({foo} toadd)
+     * - add tests
+     *   - add compile-time checks for mapping APIs in
+     *     setupDigesterComipileTimeCheck
+     *   - when adding an attribute set by bean introspection,
+     *     add to the list returned by expectedProperties()
+     * - update any client writers referring to the DOCTYPE, as necessary.
+     *   - the parent IXmlWriter should delegate to the child component
+     *     as IXmlWriter (or write the subelement itself)
+     */
+    
+    private static final String EOL = "\n";
+    
+    /** presumed relative-path to dtd file for any XML files written by writeSuiteToXmlFile */
+    public static final String DTD_PATH = "../tests/ajcTestSuite.dtd";
+    
+    /** expected doc type of AjcSpec XML files */
+    public static final String DOCTYPE = "<!DOCTYPE " 
+        + AjcTest.Suite.Spec.XMLNAME + " SYSTEM \"" + DTD_PATH + "\">";
+
+    /** xml leader */
+    public static final String FILE_LEADER
+        = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
+
+    /** @return a String suitable as an inlined DOCTYPE statement */
+    public static String inlineDocType() {
+        return "<!DOCTYPE " 
+            + AjcTest.Suite.Spec.XMLNAME 
+            + " [" 
+            + AjcSpecXmlReader.getDocType() 
+            + EOL + "   ]>";
+    }
+
+    /** 
+     * @return the elements of a document type as a String,
+     * using EOL as a line delimiter
+     */
+    public static String getDocType() {
+        if (true) {
+            throw new Error("XXX using ajcTestSuite.dtd");
+        }
+        StringBuffer r = new StringBuffer();
+        final String suiteX = AjcTest.Suite.Spec.XMLNAME;
+        final String ajctestX = AjcTest.Spec.XMLNAME;
+        final String compileX = CompilerRun.Spec.XMLNAME;
+        final String inccompileX = IncCompilerRun.Spec.XMLNAME;
+        final String runX = JavaRun.Spec.XMLNAME;
+        final String dirchangesX = DirChanges.Spec.XMLNAME;
+        final String messageX = SoftMessage.XMLNAME;
+
+        r.append(EOL + "   <!ELEMENT " + suiteX + " (" + ajctestX + "+)>");
+        r.append(EOL + "   <!ATTLIST " + suiteX + " suiteDir CDATA #IMPLIED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT " + ajctestX + " (" + compileX + ", (" + compileX + " | " + inccompileX + " | " + runX + ")*)>");
+        r.append(EOL + "   <!ATTLIST " + ajctestX + " title CDATA #REQUIRED >");
+        r.append(EOL + "   <!ATTLIST " + ajctestX + " dir CDATA #REQUIRED >");
+        r.append(EOL + "   <!ATTLIST " + ajctestX + " pr CDATA #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + ajctestX + " keywords CDATA #IMPLIED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT " + compileX + " (" + dirchangesX + "*,file*," + messageX + "*)>"); // deprecate file?
+        r.append(EOL + "   <!ATTLIST " + compileX + " staging CDATA #IMPLIED >");      // if precursor to incremental
+        r.append(EOL + "   <!ATTLIST " + compileX + " files CDATA #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + compileX + " options CDATA #IMPLIED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT " + inccompileX + " (" + dirchangesX + "*," + messageX + "*)>");  // add file* if not deprecated
+        r.append(EOL + "   <!ATTLIST " + inccompileX + " tag CDATA #REQUIRED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT " + runX + " (" + dirchangesX + "*," + messageX + "*)>");
+        r.append(EOL + "   <!ATTLIST " + runX + " class CDATA #REQUIRED >");
+        r.append(EOL + "   <!ATTLIST " + runX + " skipTester CDATA #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + runX + " options CDATA #IMPLIED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT file (#PCDATA)>");                // deprecate?
+        r.append(EOL + "   <!ATTLIST file path CDATA #IMPLIED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT " + messageX + " (#PCDATA)>");
+        r.append(EOL + "   <!ATTLIST " + messageX + " kind (error | warning | info | Xlint) #REQUIRED >");
+        r.append(EOL + "   <!ATTLIST " + messageX + " line CDATA #REQUIRED >");
+        r.append(EOL + "   <!ATTLIST " + messageX + " text CDATA #IMPLIED >");  // but Message requires non-null...
+        r.append(EOL + "   <!ATTLIST " + messageX + " file CDATA #IMPLIED >");
+        r.append(EOL + "");
+        r.append(EOL + "   <!ELEMENT " + dirchangesX + " (#PCDATA)>");
+        r.append(EOL + "   <!ATTLIST " + dirchangesX + " dirToken (classes | run) #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + dirchangesX + " defaultSuffix (.class) #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + dirchangesX + " added CDATA #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + dirchangesX + " removed CDATA #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + dirchangesX + " updated CDATA #IMPLIED >");
+        r.append(EOL + "   <!ATTLIST " + dirchangesX + " unchanged CDATA #IMPLIED >");
+        r.append(EOL + "");
+        return r.toString();
+    }
+
+    private static final AjcSpecXmlReader ME 
+        = new AjcSpecXmlReader();
+    
+    /** @return shared instance */
+    public static final AjcSpecXmlReader getReader() {
+        return ME;
+    }
+    
+    public static void main(String[] a) throws IOException {
+        writeDTD(new File("c:/home/wes/aj/aspectj/modules/tests/ajcTestSuite2.dtd"));
+    }
+    /** 
+     * Write a DTD to dtdFile.
+     * @param dtdFile the File to write to
+     */
+    public static void writeDTD(File dtdFile) throws IOException {
+        LangUtil.throwIaxIfNull(dtdFile, "dtdFile");
+        PrintWriter out = new PrintWriter(new FileWriter(dtdFile));
+        try {
+            out.println("<!-- document type for ajc test suite - see " 
+                        + AjcSpecXmlReader.class.getName() + " -->");
+            out.println(getDocType());
+        } finally {
+            out.close();
+        }        
+    }
+
+    private static final String[] LOG = new String[] {"info", "debug", "trace" };
+    
+    private int logLevel;
+    
+    private AjcSpecXmlReader() {}
+
+    /** @param level 0..2, info..trace */
+    public void setLogLevel(int level) {
+        if (level < 0) {
+            level = 0;
+        }
+        if (level > 2) {
+            level = 2;
+        }
+        logLevel = level;        
+    }
+
+    /**
+     * Print an IXmlWritable to the output file
+     * with our leader and DOCTYPE.
+     * @param output the File to write to - overwritten
+     * @param tests the List of IXmlWritable to write
+     * @return null if no warnings detected, warnings otherwise
+     */
+    public String writeSuiteToXmlFile(File output, IXmlWritable topNode) throws IOException {
+        PrintWriter writer = new PrintWriter(new FileOutputStream(output));
+        XMLWriter printSink = new XMLWriter(writer);
+        writer.println("");
+        writer.println(AjcSpecXmlReader.DOCTYPE);
+        writer.println("");
+        topNode.writeXml(printSink);
+        writer.close();
+        String parent = output.getParent();
+        if (null == parent) {
+            parent = ".";
+        }        
+        String dtdPath = parent + "/" + DTD_PATH;
+        File dtdFile = new File(dtdPath);
+        if (!dtdFile.canRead()) {
+            return "expecting dtd file: " + dtdFile.getPath();
+        }
+        return null;
+    }
+    
+    
+    /** 
+     * Read the specifications for a suite of AjcTest from an XML file.
+     * This also sets the suite dir in the specification.
+     * @param file the File must be readable, comply with DOCTYPE.
+     * @return AjcTest.Suite.Spec read from file
+     * @see setLogLevel(int)
+     */
+    public AjcTest.Suite.Spec readAjcSuite(File file) throws IOException, AbortException {
+        // setup loggers for digester and beanutils...
+        System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); // XXX
+        System.setProperty("org.apache.commons.logging.simplelog.defaultlog", LOG[logLevel]); // trace debug XXX
+
+        final Digester digester = new Digester();
+        setupDigester(digester);
+        
+        SuiteHolder holder = new SuiteHolder();
+        digester.push(holder);
+        FileInputStream input = new FileInputStream(file);
+        try {
+            digester.parse(input);
+        } catch (SAXException e) {
+            MessageUtil.fail("parsing " + file, e);
+        } finally {
+            if (null != input) {
+                input.close();
+                input = null;
+            }            
+        }
+        AjcTest.Suite.Spec result = holder.spec;
+        if (null != result) {
+            File suiteDir = file.getParentFile();
+            if (null == suiteDir) {
+                suiteDir = new File("."); // user.dir?
+            }
+            result.setSuiteDirFile(suiteDir);
+            if (result.runtime.isVerbose()) { // XXX hack fixup
+                RunUtils.enableVerbose(result);
+            }
+        }
+        return result;
+    }
+   
+    /** set up the mapping between the xml and Java. */
+    private void setupDigester(Digester digester) {
+        // XXX supply sax parser to ignore white space?
+        digester.setValidating(true);
+
+        // element names come from the element components
+        final String suiteX = AjcTest.Suite.Spec.XMLNAME;
+        final String ajctestX = suiteX + "/" + AjcTest.Spec.XMLNAME;
+        final String compileX = ajctestX + "/" + CompilerRun.Spec.XMLNAME;
+        final String inccompileX = ajctestX + "/" + IncCompilerRun.Spec.XMLNAME;
+        final String runX = ajctestX + "/" + JavaRun.Spec.XMLNAME;
+        final String dirchangesX = "*/" + DirChanges.Spec.XMLNAME;
+        final String messageX = "*/" + SoftMessage.XMLNAME;
+        final String messageSrcLocX = messageX + "/source-location";
+
+        // ---- each sub-element needs to be created
+        // handle messages the same at any level
+        digester.addObjectCreate(suiteX,               AjcTest.Suite.Spec.class.getName());
+        digester.addObjectCreate(ajctestX,             AjcTest.Spec.class.getName());
+        digester.addObjectCreate(compileX,             CompilerRun.Spec.class.getName());
+        digester.addObjectCreate(compileX + "/file",   AbstractRunSpec.WrapFile.class.getName());
+        digester.addObjectCreate(inccompileX,          IncCompilerRun.Spec.class.getName());
+        digester.addObjectCreate(runX,                 JavaRun.Spec.class.getName()); 
+        digester.addObjectCreate(messageX,             SoftMessage.class.getName());
+        digester.addObjectCreate(messageSrcLocX,       SoftSourceLocation.class.getName());
+        digester.addObjectCreate(dirchangesX,          DirChanges.Spec.class.getName());
+        
+        // ---- set bean properties for sub-elements created automatically
+        // -- some remapped - warnings
+        //   - if property exists, map will not be used
+        digester.addSetProperties(suiteX); // ok to have suite messages and global suite options, etc.
+        digester.addSetProperties(ajctestX, 
+            new String[] { "title", "dir", "pr"},
+            new String[] { "description", "testDirOffset", "bugId"});
+        digester.addSetProperties(compileX, 
+            new String[] { "files", "argfiles"},
+            new String[] { "paths", "argfiles"});
+        digester.addSetProperties(compileX + "/file");
+        digester.addSetProperties(inccompileX, "classes", "paths");
+        digester.addSetProperties(runX, //"class", "className");
+            new String[] { "class", "vm", "skipTester"},
+            new String[] { "className", "javaVersion", "skipTester"});
+        digester.addSetProperties(dirchangesX);
+        digester.addSetProperties(messageX);
+        digester.addSetProperties(messageSrcLocX);
+        digester.addSetProperties(messageX, "kind", "kindAsString");
+        digester.addSetProperties(messageX, "line", "lineAsString");
+        // only file subelement of compile uses text as path... XXX vestigial
+        digester.addCallMethod(compileX + "/file", "setFile", 0);
+
+        // ---- when subelements are created, add to parent 
+        // add ajctest to suite, runs to ajctest, files to compile, messages to any parent...
+        // the method name (e.g., "addSuite") is in the parent (SuiteHolder)
+        // the class (e.g., AjcTest.Suite.Spec) refers to the type of the object created
+        digester.addSetNext(suiteX,               "addSuite", AjcTest.Suite.Spec.class.getName());
+        digester.addSetNext(ajctestX,             "addChild", AjcTest.Spec.class.getName());
+        digester.addSetNext(compileX,             "addChild", CompilerRun.Spec.class.getName());
+        digester.addSetNext(inccompileX,          "addChild", IncCompilerRun.Spec.class.getName());
+        digester.addSetNext(runX,                 "addChild", JavaRun.Spec.class.getName());
+        digester.addSetNext(compileX + "/file",   "addWrapFile", AbstractRunSpec.WrapFile.class.getName());
+        digester.addSetNext(messageX,             "addMessage", IMessage.class.getName());
+        digester.addSetNext(messageSrcLocX,       "setSourceLocation", ISourceLocation.class.getName());
+        digester.addSetNext(dirchangesX,          "addDirChanges", DirChanges.Spec.class.getName());
+        
+        // can set parent, but prefer to have "knows-about" flow down only...
+    }
+
+    // ------------------------------------------------------------ testing code
+    /** 
+     * Get expected bean properties for introspection tests. 
+     * This should return an expected property for every attribute in DOCTYPE,
+     * using any mapped-to names from setupDigester.
+     */
+    static BProps[] expectedProperties() {
+        return new BProps[] 
+            {
+                new BProps(AjcTest.Suite.Spec.class, 
+                    new String[] { "suiteDir"}), // verbose removed
+                new BProps(AjcTest.Spec.class, 
+                    new String[] { "description", "testDirOffset", "bugId"}),
+                    // mapped from { "title", "dir", "pr"}
+                new BProps(CompilerRun.Spec.class, 
+                    new String[] { "files", "options"}),
+                new BProps(IncCompilerRun.Spec.class, 
+                    new String[] { "tag" }),
+                new BProps(JavaRun.Spec.class, 
+                    new String[] { "className", "skipTester", "options"}),
+                    // mapped from { "class", ...}
+                new BProps(DirChanges.Spec.class, 
+                    new String[] { "added", "removed", "updated", "unchanged", "dirToken", "defaultSuffix"}),
+                new BProps(AbstractRunSpec.WrapFile.class, 
+                    new String[] { "path"}),
+                new BProps(SoftMessage.class, 
+                    new String[] { "kindAsString", "lineAsString", "text", "file"})
+                    // mapped from { "kind", "line", ...}
+            };
+    }
+        
+    /** 
+     * This is only to do compile-time checking for the APIs impliedly
+     * used in setupDigester(..).
+     * The property setter checks are redundant with tests based on
+     * expectedProperties().
+     */
+    private static void setupDigesterCompileTimeCheck() { 
+        if (true) { throw new Error("never invoked"); }
+        AjcTest.Suite.Spec suite = new AjcTest.Suite.Spec();
+        AjcTest.Spec test = new AjcTest.Spec();
+//        AjcTest test = new AjcTest();
+//        test.addRunSpec((AbstractRunSpec) null);
+////        test.makeIncCompilerRun((IncCompilerRun.Spec) null);
+////        test.makeJavaRun((JavaRun.Spec) null);
+//        test.setDescription((String) null);
+//        test.setTestBaseDirOffset((String) null);
+//        test.setBugId((String) null);
+//        test.setTestSourceLocation((ISourceLocation) null);
+            
+        CompilerRun.Spec crunSpec = new CompilerRun.Spec();
+        crunSpec.addMessage((IMessage) null);
+        // XXX crunSpec.addSourceLocation((ISourceLocation) null);
+        crunSpec.addWrapFile((AbstractRunSpec.WrapFile) null);
+        crunSpec.setOptions((String) null);
+        crunSpec.setPaths((String) null);
+        
+        IncCompilerRun.Spec icrunSpec = new IncCompilerRun.Spec();
+        icrunSpec.addMessage((IMessage) null);
+        icrunSpec.setTag((String) null);
+
+        JavaRun.Spec jrunspec = new JavaRun.Spec();
+        jrunspec.addMessage((IMessage) null);
+        jrunspec.setClassName((String) null);
+        jrunspec.addMessage((IMessage) null);
+        // input s.b. interpretable by Boolean.valueOf(String)
+        jrunspec.setSkipTester(true); 
+
+        DirChanges.Spec dcspec = new DirChanges.Spec();
+        dcspec.setAdded((String) null);
+        dcspec.setRemoved((String) null);
+        dcspec.setUpdated((String) null);
+        dcspec.setDefaultSuffix((String) null);
+        dcspec.setDirToken((String) null);
+
+        SoftMessage m = new SoftMessage();
+        m.setSourceLocation((ISourceLocation) null);
+        m.setText((String) null);
+        m.setKindAsString((String) null);
+        
+        SoftSourceLocation sl = new SoftSourceLocation();
+        sl.setFile((String) null); 
+        sl.setLine((String) null); 
+        sl.setColumn((String) null); 
+        sl.setEndLine((String) null); 
+        
+        // add attribute setters to validate?
+    }
+    
+    /** top element on Digester stack holds the test suite */
+    public static class SuiteHolder {
+        AjcTest.Suite.Spec spec;
+        public void addSuite(AjcTest.Suite.Spec spec) {
+            this.spec = spec;
+        }
+    }
+    
+    /** hold class/properties association for testing */
+    static class BProps {
+        final Class cl;
+        final String[] props;
+        BProps(Class cl, String[] props) {
+            this.cl = cl;
+            this.props = props;
+        }
+    }
+}
+
+
diff --git a/testing/src/org/aspectj/testing/xml/IXmlWritable.java b/testing/src/org/aspectj/testing/xml/IXmlWritable.java
new file mode 100644 (file)
index 0000000..52f43c3
--- /dev/null
@@ -0,0 +1,25 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.xml;
+
+/**
+ * 
+ */
+public interface IXmlWritable {
+    /** 
+     * Write self out to XML.
+     * @param out the XMLWriter to write to
+     */
+    void writeXml(XMLWriter out);
+}
diff --git a/testing/src/org/aspectj/testing/xml/SoftMessage.java b/testing/src/org/aspectj/testing/xml/SoftMessage.java
new file mode 100644 (file)
index 0000000..e57a4f3
--- /dev/null
@@ -0,0 +1,287 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.xml;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.bridge.SourceLocation;
+import org.aspectj.util.LangUtil;
+
+import java.io.File;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Implement messages.
+ * This implementation is immutable if ISourceLocation is immutable.
+ */
+public class SoftMessage implements IMessage { // XXX mutable dup of Message
+    public static String XMLNAME = "message";
+    public static final File NO_FILE = ISourceLocation.NO_FILE;
+    private String message;
+    private IMessage.Kind kind;
+    private Throwable thrown;
+    private ISourceLocation sourceLocation;
+    
+
+       //private ISourceLocation pseudoSourceLocation;  // set directly
+    // collapse enclosed source location for shorter, property-based xml
+    private String file;
+    private int line = Integer.MAX_VALUE;
+    
+    /** convenience for constructing failure messages */
+    public static SoftMessage fail(String message, Throwable thrown) {
+        return new SoftMessage(message, IMessage.FAIL, thrown, null);
+    }
+
+    /** 
+     * Print messages. 
+     * @param messages List of IMessage
+     */
+    public static void writeXml(XMLWriter out, List messages) {
+        if ((null == out) || (null == messages)) {
+            return;
+        }
+        for (Iterator iter = messages.iterator(); iter.hasNext();) {
+            writeXml(out, (IMessage) iter.next());
+            
+        }
+    }
+
+    /** 
+     * Print messages. 
+     * @param messages IMessage[] 
+     */
+    public static void writeXml(XMLWriter out, IMessage[] messages) {
+        if ((null == out) || (null == messages)) {
+            return;
+        }
+        for (int i = 0; i < messages.length; i++) {
+            writeXml(out, messages[i]);            
+        }
+    }
+
+    /** print message as an element */
+    public static void writeXml(XMLWriter out, IMessage message) { // XXX short form only, no files
+       if ((null == out) || (null == message)) {
+            return;
+       }
+       final String elementName = XMLNAME;
+       String kindStr = message.getKind().toString();
+       String kindAttr = out.makeAttribute("kind",kindStr);
+       String mStr = message.getMessage();
+       if ((null != mStr) && (0 == mStr.length())) {
+            mStr = null;
+       }
+       String mAttr = (null == mStr ? " " : " " + out.makeAttribute("text", mStr));
+       int mAttrLen = (null == mStr ? 0 : mAttr.length());
+       ISourceLocation sl = message.getISourceLocation();
+       String lineStr = (null == sl ? null : "" + sl.getLine());
+       String lineAttr = (null == lineStr ? " " : " " + out.makeAttribute("line", lineStr));
+       int lineAttrLen = (null == lineStr ? 0 : lineAttr.length());
+       int len = (kindAttr.length() + mAttrLen + lineAttrLen);
+       if (len < 65) {
+            String s = kindAttr + " "
+                        + ((null == lineStr ? "" : lineAttr)
+                           + (null == mStr ? "" : " " + mAttr)).trim();
+            out.printElement(elementName, s);
+       } else {
+            out.startElement(elementName, kindAttr + lineAttr, false);
+            if (0 < mStr.length()) {
+                out.printAttribute("text", mStr);
+            }
+            out.endAttributes();
+            out.endElement(elementName);
+       }
+    }
+
+
+    
+    public SoftMessage() {} // XXX programmatic only
+    
+    /**
+     * Create a (compiler) error or warning message
+     * @param message the String used as the underlying message
+     * @param sourceLocation the ISourceLocation, if any, associated with this message
+     * @param isError if true, use IMessage.ERROR; else use IMessage.WARNING
+     */
+    public SoftMessage(String message, ISourceLocation location, boolean isError) {
+        this(message, (isError ? IMessage.ERROR : IMessage.WARNING), null,
+            location);
+    }
+    
+    /**
+     * Create a message, handling null values for message and kind
+     * if thrown is not null.
+     * @param message the String used as the underlying message
+     * @param kind the IMessage.Kind of message - not null
+     * @param thrown the Throwable, if any, associated with this message
+     * @param sourceLocation the ISourceLocation, if any, associated with this message
+     * @throws IllegalArgumentException if message is null and
+     * thrown is null or has a null message, or if kind is null
+     * and thrown is null.
+     */
+    public SoftMessage(String message, IMessage.Kind kind, Throwable thrown,
+                    ISourceLocation sourceLocation) {
+        this.message = message;
+        this.kind = kind;
+        this.thrown = thrown;
+        this.sourceLocation = sourceLocation;
+        if (null == message) {
+            if (null != thrown) {
+                message = thrown.getMessage();
+            } 
+            if (null == message) {
+                throw new IllegalArgumentException("null message");
+            }
+        }
+        if (null == kind) {
+             throw new IllegalArgumentException("null kind");
+        }
+    }
+    
+    /** @return the kind of this message */
+    public IMessage.Kind getKind() {
+        return kind;
+    }
+
+    /** @return true if kind == IMessage.ERROR */
+    public boolean isError() {
+        return kind == IMessage.ERROR;
+    }
+    
+    /** @return true if kind == IMessage.WARNING */
+    public boolean isWarning() {
+        return kind == IMessage.WARNING;
+    }
+
+    /** @return true if kind == IMessage.DEBUG */
+    public boolean isDebug() {
+        return kind == IMessage.DEBUG;
+    }
+
+    /** 
+     * @return true if kind == IMessage.INFO  
+     */
+    public boolean isInfo() {
+        return kind == IMessage.INFO;
+    }
+    
+    /** @return true if  kind == IMessage.ABORT  */
+    public boolean isAbort() {
+        return kind == IMessage.ABORT;
+    }    
+    
+    /** 
+     * @return true if kind == IMessage.FAIL
+     */
+    public boolean isFailed() {
+        return kind == IMessage.FAIL;
+    }
+    
+    /** @return non-null String with simple message */
+    final public String getMessage() {
+        return message;
+    }
+    
+    /** @return Throwable associated with this message, or null if none */
+    final public Throwable getThrown() {
+        return thrown;
+    }
+
+    /** 
+     * This returns any ISourceLocation set or a mock-up
+     * if file and/or line were set.
+     * @return ISourceLocation associated with this message, 
+     * a mock-up if file or line is available, or null if none 
+     */
+    final public ISourceLocation getISourceLocation() {
+        if ((null == sourceLocation) 
+            && ((null != file) || (line != Integer.MAX_VALUE))) {
+            File f = (null == file ? NO_FILE : new File(file));
+            int line = (this.line == Integer.MAX_VALUE ? 0 : this.line);
+            sourceLocation = new SourceLocation(f, line);
+        }
+        return sourceLocation;
+    }
+    
+    /** set the kind of this message */
+    public void setMessageKind(IMessage.Kind kind) {
+        this.kind = (null == kind ? IMessage.ERROR : kind);
+    }
+
+
+    /** set the file for the underlying source location of this message
+     * @throws IllegalStateException if source location was set directly
+     *          or indirectly by calling getSourceLocation after setting
+     *          file or line.
+     */
+    public void setFile(String path) {
+        LangUtil.throwIaxIfFalse(!LangUtil.isEmpty(path), "empty path");
+        if (null != sourceLocation) {
+            throw new IllegalStateException("cannot set line after creating source location");
+        }
+        this.file = path;
+    }
+
+    /** set the kind of this message */
+    public void setKindAsString(String kind) {
+        setMessageKind(MessageUtil.getKind(kind));
+    }
+
+    public void setSourceLocation(ISourceLocation sourceLocation) {
+        this.sourceLocation = sourceLocation;
+    }
+    
+    /** 
+     * Set the line for the underlying source location.
+     * @throws IllegalStateException if source location was set directly
+     *          or indirectly by calling getSourceLocation after setting
+     *          file or line.
+     */
+    public void setLineAsString(String line) {
+        if (null != sourceLocation) {
+            throw new IllegalStateException("cannot set line after creating source location");
+        }
+        this.line = Integer.valueOf(line).intValue();
+        SourceLocation.validLine(this.line);
+    }
+
+    public void setText(String text) {
+        this.message = (null == text ? "" : text);
+    }
+    
+    public String toString() {
+        StringBuffer result = new StringBuffer();
+        
+        result.append(getKind().toString());
+        
+        String messageString = getMessage();
+        if (!LangUtil.isEmpty(messageString)) {
+            result.append(messageString);
+        }
+
+        ISourceLocation loc = getISourceLocation();
+        if ((null != loc) && (loc != ISourceLocation.NO_FILE)) {
+            result.append(" at " + loc);
+        }
+        if (null != thrown) {
+            result.append(" -- " + LangUtil.renderExceptionShort(thrown));
+        }
+        return result.toString();
+    }    
+}
diff --git a/testing/src/org/aspectj/testing/xml/SoftSourceLocation.java b/testing/src/org/aspectj/testing/xml/SoftSourceLocation.java
new file mode 100644 (file)
index 0000000..2b3dfda
--- /dev/null
@@ -0,0 +1,89 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.xml;
+
+
+import org.aspectj.bridge.ISourceLocation;
+
+import java.io.File;
+
+/**
+ * Immutable source location.
+ * This guarantees that the source file is not null
+ * and that the numeric values are positive and line <= endLine.
+ * @see org.aspectj.lang.reflect.SourceLocation
+ * @see org.aspectj.compiler.base.parser.SourceInfo
+ * @see org.aspectj.tools.ide.SourceLine
+ * @see org.aspectj.testing.harness.ErrorLine
+ */
+public class SoftSourceLocation implements ISourceLocation  { // XXX endLine?
+    public static final File NONE = new File("SoftSourceLocation.NONE");
+    private File sourceFile;
+    private int line;
+    private int column;
+    private int endLine;
+    
+    public SoftSourceLocation() {
+    }
+    
+    public File getSourceFile() {
+        return (null != sourceFile ? sourceFile : NONE);
+    }
+    public int getLine() {
+        return line;
+    }
+    
+    public int getColumn() {
+        return column;
+    }
+    
+    public int getEndLine() {
+        return line;
+    }
+    
+    public void setFile(String sourceFile) {
+        this.sourceFile = new File(sourceFile);
+    }
+
+    public void setLine(String  line) {
+        this.line = convert(line);
+        if (0 == endLine) {
+            endLine = this.line;
+        }
+    }
+    
+    public void setColumn(String column) {
+        this.column = convert(column);
+    }
+    
+    public void setEndLine(String line) {
+        this.endLine = convert(line);
+    }
+    
+    
+    private int convert(String in) {
+        return Integer.valueOf(in).intValue();
+    }
+
+       public String getLocationContext() {
+               return null;
+       }
+    
+    /** @return String : file:line:column */
+    public String toString() {
+       return getSourceFile().getPath() + ":" + getLine() + ":" + getColumn();
+    }
+
+
+}
diff --git a/testing/src/org/aspectj/testing/xml/XMLWriter.java b/testing/src/org/aspectj/testing/xml/XMLWriter.java
new file mode 100644 (file)
index 0000000..fc2eda9
--- /dev/null
@@ -0,0 +1,353 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.xml;
+
+import org.aspectj.util.LangUtil;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Stack;
+
+/** 
+ * Manage print stream to an XML document.
+ * This tracks start/end elements and signals on error,
+ * and optionally lineates buffer by accumulating
+ * up to a maximum width (including indent).
+ * This also has utilities for un/flattening lists and
+ * rendering buffer.
+ */
+public class XMLWriter {
+    static final String SP = "  ";
+    static final String TAB = SP + SP;
+    
+    /** maximum value for maxWidth, when flowing buffer */
+    public static final int MAX_WIDTH = 8000;
+
+    /** default value for maxWidth, when flowing buffer */
+    public static final int DEFAULT_WIDTH = 80;
+
+    /** currently this just strips quotes and ampersands */
+    public static String attributeValue(String input) {
+        input = input.replace('"', '~');
+        input = input.replace('&', '=');
+        return input;
+    }
+    
+    /** @return name="{attributeValue({value})" */
+    public static String makeAttribute(String name, String value) {
+        return (name + "=\"" + attributeValue(value) + "\"");
+    }
+    
+    /** same as flattenList, except also normalize \ -> / */
+    public static String flattenFiles(String[] strings) {
+        return flattenList(strings).replace('\\', '/');
+    }
+    
+    /** same as flattenList, except also normalize \ -> / */
+    public static String flattenFiles(List paths) {
+        return flattenList(paths).replace('\\', '/');
+    }
+
+    /**
+     * Expand comma-delimited String into list of values, without trimming
+     * @param list List of items to print - null is silently ignored,
+     *         so for empty items use ""
+     * @return String[]{} for null input, String[] {input} for input without comma,
+     *          or new String[] {items..} otherwise
+     * @throws IllegalArgumentException if {any item}.toString() contains a comma
+     */
+    public static String[] unflattenList(String input) {
+        return (String[]) LangUtil.commaSplit(input).toArray(new String[0]);
+    }
+    
+    /** flatten input and add to list */
+    public static void addFlattenedItems(List list, String input) {
+        LangUtil.throwIaxIfNull(list, "list");
+        if (null != input) {
+            String[] items = XMLWriter.unflattenList(input);
+            if (!LangUtil.isEmpty(items)) {
+                for (int i = 0; i < items.length; i++) {
+                    if (!LangUtil.isEmpty(items[i])) {
+                        list.add(items[i]);
+                    }
+                }
+            }
+        }    
+    }
+    
+       /** 
+     * Collapse list into a single comma-delimited value (e.g., for list buffer)
+     * @param list List of items to print - null is silently ignored,
+     *         so for empty items use ""
+     * @return item{,item}...
+     * @throws IllegalArgumentException if {any item}.toString() contains a comma
+     */
+    public static String flattenList(List list) {
+        if ((null == list) || (0 == list.size())) {
+            return "";
+        }
+        return flattenList(list.toArray());
+    }
+
+
+       /** 
+     * Collapse list into a single comma-delimited value (e.g., for list buffer)
+     * @param list the String[] items to print - null is silently ignored,
+     *         so for empty items use ""
+     * @return item{,item}...
+     * @throws IllegalArgumentException if list[i].toString() contains a comma
+     */
+    public static String flattenList(Object[] list) {
+        StringBuffer sb = new StringBuffer();
+        if (null != list) {
+            boolean printed = false;
+            for (int i = 0; i < list.length; i++) {
+                Object o = list[i];
+                if (null != o) {
+                    if (printed) {
+                        sb.append(",");
+                    } else {
+                        printed = true;
+                    }
+                    String s = o.toString();
+                    if (-1 != s.indexOf(",")) {
+                        throw new IllegalArgumentException("comma in " + s);
+                    }
+                    sb.append(s);
+                }
+                       }
+        }
+        return sb.toString();
+    }
+    
+    /** output sink */
+    PrintWriter out;
+    
+    /** stack of String element names */
+    Stack stack = new Stack(); 
+    
+    /** false if doing attributes */
+    boolean attributesDone = true;
+    
+    /** current element prefix */
+    String indent = "";
+    
+    /** maximum width (in char) of indent and buffer when flowing */
+    int maxWidth;
+    
+    /** 
+     * Current text being flowed.
+     * length() is always less than maxWidth.
+     */
+    StringBuffer buffer;
+
+    /** @param out PrintWriter to print to - not null */
+    public XMLWriter(PrintWriter out) {
+        LangUtil.throwIaxIfNull(out, "out");
+        this.out = out;
+        buffer = new StringBuffer();
+        maxWidth = DEFAULT_WIDTH;
+    }
+    
+    /** 
+     * Set maximum width (in chars) of buffer to accumulate.
+     * @param maxWidth int 0..MAX_WIDTH for maximum number of char to accumulate 
+     */
+    public void setMaxWidth(int maxWidth) {
+        if (0 > maxWidth) {
+            this.maxWidth = 0;
+        } else if (MAX_WIDTH < maxWidth) {
+            this.maxWidth = MAX_WIDTH;
+        } else {
+            this.maxWidth = maxWidth;
+        }
+    }
+    
+    /** shortcut for entire element */
+    public void printElement(String name, String attributes) {
+        if (!attributesDone) throw new IllegalStateException("finish attributes");
+        if (0 != buffer.length()) {  // output on subelement
+            outPrintln(buffer + ">");
+            buffer.setLength(0);
+        }
+        String oldIndent = indent;
+        if (0 < stack.size()) {
+            indent += TAB;
+            ((StackElement) stack.peek()).numChildren++;
+        }
+        outPrintln(indent + "<" + name + " " + attributes + "/>");
+        indent = oldIndent;
+    }
+
+    /**
+     * Start element only
+     * @param name the String label of the element
+     * @param closeTag if true, delimit the end of the starting tag
+     */
+    public void startElement(String name, boolean closeTag) {
+        startElement(name, "", closeTag);
+    }
+    
+    /** 
+     * Start element with buffer on the same line.
+     * This does not flow buffer.
+     * @param name String tag for the element
+     * @param attr {name="value"}.. where value 
+     *         is a product of attributeValue(String)
+     */
+    public void startElement(String name, String attr, boolean closeTag) {
+        if (!attributesDone) throw new IllegalStateException("finish attributes");
+        LangUtil.throwIaxIfFalse(!LangUtil.isEmpty(name), "empty name");
+
+        if (0 != buffer.length()) { // output on subelement
+            outPrintln(buffer + ">");
+            buffer.setLength(0);
+        }
+        if (0 < stack.size()) {
+            indent += TAB;
+        }
+        StringBuffer sb = new StringBuffer();
+        sb.append(indent);
+        sb.append("<");
+        sb.append(name);
+        
+        if (!LangUtil.isEmpty(attr)) {
+            sb.append(" ");
+            sb.append(attr.trim());
+        }
+        attributesDone = closeTag;
+        if (closeTag) {
+            sb.append(">");
+            outPrintln(sb.toString());
+        } else if (maxWidth <= sb.length()) {
+            outPrintln(sb.toString());
+        } else {
+            if (0 != this.buffer.length()) {
+                throw new IllegalStateException("expected empty attributes starting " + name);
+            }
+            this.buffer.append(sb.toString());
+        }
+        if (0 < stack.size()) {
+            ((StackElement) stack.peek()).numChildren++;
+        }
+        stack.push(new StackElement(name));
+    }
+    
+    /** 
+     * @param name should be the same as that given to start the element
+     * @throws IllegalStateException if start element does not match
+     */
+    public void endElement(String name) {
+        int level = stack.size();
+        String err = null;
+        StackElement element = null;
+        if (0 == stack.size()) {
+            err = "empty stack";
+        } else {
+            element = (StackElement) stack.pop();
+            if (!element.name.equals(name)) {
+                err = "expecting element " + element.name;
+            }
+        }
+        if (null != err) {
+            err = "endElement(" + name + ") " + stack + ": " + err;
+            throw new IllegalStateException(err);
+        }
+        if (0 < element.numChildren) {
+            outPrintln(indent + "</" + name + ">");
+        } else if (0 < buffer.length()) {
+            outPrintln(buffer + "/>");
+            buffer.setLength(0);
+        } else {
+            outPrintln(indent + "/>");
+        }
+        if (!attributesDone) {
+            attributesDone = true;
+        }
+        if (0 < stack.size()) {
+            indent = indent.substring(0,  indent.length() - TAB.length());
+        }
+    }
+    
+
+    /**
+     * Print name=value if neither is null and name is not empty after trimming,
+     * accumulating these until they are greater than maxWidth or buffer are
+     * terminated with endAttributes(..) or endElement(..).
+     * @param value the String to convert as attribute value - ignored if null
+     * @param name the String to use as the attribute name
+     * @throws IllegalArgumentException if name is null or empty after trimming
+     */
+    public void printAttribute(String name, String value) {
+        if (attributesDone) throw new IllegalStateException("not in attributes");
+        if (null == value) {
+            return;
+        }
+        if ((null == name) || (0 == name.trim().length())) {
+            throw new IllegalArgumentException("no name=" + name + "=" + value);
+        }
+        
+        String newAttr = name + "=\"" + attributeValue(value) + "\"";
+        int indentLen = indent.length();
+        int bufferLen = buffer.length();
+        int newAttrLen = (0 == bufferLen ? indentLen : 0) + newAttr.length();
+        
+        if (maxWidth > (bufferLen + newAttrLen)) {
+            buffer.append(" ");
+            buffer.append(newAttr);
+        } else {  // at least print old attributes; maybe also new
+            if (0 < bufferLen) {
+                outPrintln(buffer.toString());
+                buffer.setLength(0);
+            }
+            buffer.append(indent + SP + newAttr);
+        } 
+    }
+
+    public void endAttributes() {
+        if (attributesDone) throw new IllegalStateException("not in attributes");
+        attributesDone = true;
+    }
+    
+    public void printComment(String comment) {
+        if (!attributesDone) throw new IllegalStateException("in attributes");
+        outPrintln(indent + "<!-- " + comment + "-->");
+    }
+    
+    public void close() {
+        if (null != out) {
+            out.close();
+        }
+            
+    }
+
+       public void println(String string) {
+        outPrintln(string);
+       }
+
+    private void outPrintln(String s) {
+        if (null == out) {
+            throw new IllegalStateException("used after close");
+        }
+        out.println(s);
+    }
+    static class StackElement {
+        String name;
+        int numChildren;
+        public StackElement(String name) {
+            this.name = name;
+        }
+    }
+    
+}
diff --git a/testing/testdata/harnessList.txt b/testing/testdata/harnessList.txt
new file mode 100644 (file)
index 0000000..2b146c1
--- /dev/null
@@ -0,0 +1 @@
+@../../../tests/harnessPasses.txt
diff --git a/testing/testdata/suite.dtd b/testing/testdata/suite.dtd
new file mode 100644 (file)
index 0000000..6502567
--- /dev/null
@@ -0,0 +1,6 @@
+<!DOCTYPE suite [\r
+     <!ELEMENT suite (ajc-test+)>\r
+     <!ELEMENT ajc-test (compile+,run+)>\r
+     <!ELEMENT compile (#PCDATA)>\r
+     <!ELEMENT run (#PCDATA)>\r
+     ]>
\ No newline at end of file
diff --git a/testing/testdata/suite.xml b/testing/testdata/suite.xml
new file mode 100644 (file)
index 0000000..660af52
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+
+<!DOCTYPE suite [
+     <!ELEMENT suite (ajc-test+)>
+     <!ELEMENT ajc-test (compile*,run*)>
+     <!ATTLIST ajc-test title CDATA #REQUIRED >
+     <!ATTLIST ajc-test dir CDATA #REQUIRED >
+     <!ATTLIST ajc-test pr CDATA #IMPLIED >
+
+     <!ELEMENT compile (file*,message*)>
+     
+     <!ELEMENT run (#PCDATA)>
+     <!ATTLIST run class CDATA #REQUIRED >
+     <!ATTLIST run args CDATA "" >
+
+     <!ELEMENT file (#PCDATA)>
+     <!ELEMENT message (#PCDATA)>
+     <!ATTLIST message kind (error | warning | Xlint) #REQUIRED >
+     <!ATTLIST message line CDATA #REQUIRED >
+     <!ATTLIST message file CDATA #IMPLIED >
+     ]>
+
+
+
+<suite>
+<!--correct behavior for 1.0 is good error message, see real test in knownbugs-->
+<ajc-test title="harness error test"
+          dir="harness" pr="9998">
+<compile file="ErrorTest.java">
+<message kind="error" line="5"/>
+<message kind="error" line="6"/>
+</compile>
+</ajc-test>
+
+
+<ajc-test title="harness test with run"
+          dir="harness" pr="101">
+<compile file="TestNoTester.java"/>
+<run class="TestNoTester"/>
+</ajc-test>
+
+
+</suite>
\ No newline at end of file
diff --git a/testing/testsrc/TestingModuleTests.java b/testing/testsrc/TestingModuleTests.java
new file mode 100644 (file)
index 0000000..a07fd86
--- /dev/null
@@ -0,0 +1,31 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+// default package
+
+import junit.framework.*;
+import junit.framework.Test;
+
+public class TestingModuleTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(TestingModuleTests.class.getName());
+        suite.addTest(org.aspectj.testing.util.UtilTests.suite()); 
+        suite.addTest(org.aspectj.testing.xml.TestingXmlTests.suite()); 
+        return suite;
+    }
+
+    public TestingModuleTests(String name) { super(name); }
+
+}  
diff --git a/testing/testsrc/org/aspectj/testing/harness/bridge/AbstractRunSpecTest.java b/testing/testsrc/org/aspectj/testing/harness/bridge/AbstractRunSpecTest.java
new file mode 100644 (file)
index 0000000..0e5b865
--- /dev/null
@@ -0,0 +1,65 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.xml.XMLWriter;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class AbstractRunSpecTest extends TestCase {
+
+       public AbstractRunSpecTest(String name) {
+               super(name);
+       }
+
+    public void skiptestXmlWrite() {
+        AbstractRunSpec spec = new TestSpec();
+        spec.setOptions("-option1,-option2");
+        spec.setKeywords("keyword1, keyword2");
+        spec.setPaths("path1.java, path2.java");
+        spec.setDescription("some description, with extra");
+        XMLWriter out = new XMLWriter(new PrintWriter(System.out));
+        spec.writeXml(out);
+        out.close();
+    }
+
+    public void testSetOptions() {
+        AbstractRunSpec spec = new TestSpec();
+        spec.setOptions("1,2");
+        List options = spec.getOptionsList();
+        String s = "" + options;
+        assertTrue(s, "[1, 2]".equals(s));
+    }
+    
+    static class TestSpec extends AbstractRunSpec {
+        TestSpec() {
+            super("testspec");            
+        }
+        /**
+                * @see org.aspectj.testing.harness.bridge.AbstractRunSpec#makeRunIterator(Sandbox, Validator)
+                */
+               public IRunIterator makeRunIterator(
+                       Sandbox sandbox,
+                       Validator validator) {
+                       return null;
+               }
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/harness/bridge/AjcSpecTest.java b/testing/testsrc/org/aspectj/testing/harness/bridge/AjcSpecTest.java
new file mode 100644 (file)
index 0000000..6357901
--- /dev/null
@@ -0,0 +1,340 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.harness.bridge.AjcTest.Suite.Spec;
+import org.aspectj.testing.xml.AjcSpecXmlReaderTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import junit.framework.Assert;
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+/**
+ * Primarily used by others to test AjcTest
+ */
+public class AjcSpecTest extends TestCase {
+    public static final String NOTSAME = " != ";
+    public static void sameAjcSuiteSpec(
+        AjcTest.Suite.Spec lhsSpec,
+        AjcTest.Suite.Spec rhsSpec,
+        Assert a) {
+        assertNotNull(lhsSpec);
+        assertNotNull(rhsSpec);
+        Iterator lhs = lhsSpec.getChildren().iterator();
+        Iterator rhs = rhsSpec.getChildren().iterator();
+        while (lhs.hasNext() && rhs.hasNext()) {
+            AjcTest.Spec lhsTest = (AjcTest.Spec) lhs.next();
+            AjcTest.Spec rhsTest = (AjcTest.Spec) rhs.next();
+            AjcSpecTest.sameAjcTestSpec(lhsTest, rhsTest, a);
+        }
+        a.assertTrue(!lhs.hasNext());        
+        a.assertTrue(!rhs.hasNext());        
+    }
+
+       public static void sameAjcTestSpec(
+               AjcTest.Spec lhsTest,
+               AjcTest.Spec rhsTest,
+               Assert a) {
+        a.assertNotNull(lhsTest);
+        a.assertNotNull(rhsTest);
+        a.assertEquals(lhsTest.getBugId(), rhsTest.getBugId());
+        a.assertEquals(lhsTest.getTestDirOffset(), rhsTest.getTestDirOffset());
+        // XXX suiteDir varies by run..
+        sameAbstractRunSpec(lhsTest, rhsTest, a);
+       }
+
+       public static void sameAbstractRunSpec(
+               AbstractRunSpec lhs,
+               AbstractRunSpec rhs,
+        Assert a) {
+        a.assertEquals(lhs.description, rhs.description);
+        // XXX keywords added in .txt reading - 
+        //sameList(lhs.getKeywordsList(), rhs.getKeywordsList(), a);
+        // XXX sameList(lhs.globalOptions, rhs.globalOptions, a);
+        sameList(lhs.getOptionsList(), rhs.getOptionsList(), a);
+        sameList(lhs.getPathsList(), rhs.getPathsList(), a);
+        // xml adds sourceloc?
+        //sameSourceLocation(lhs.getSourceLocation(), rhs.getSourceLocation(), a);
+        // XXX also sourceLocations?
+        sameMessages(lhs.getMessages(), rhs.getMessages(), a);
+       }
+
+    /** @return normal form - null is "", "" is "", and others are {fully.qualified.class}.toString().trim() */
+    static String normal(Object input) {
+        if ((null == input) || ("".equals(input))) {
+            return "";
+        } else {
+            return input.getClass().getName() + "." + input.toString().trim();
+        }
+    }
+
+    /** @return true if these match after normalizing */
+    public static void same(Object lhs, Object rhs, Assert a) {
+        lhs = normal(lhs);
+        rhs = normal(rhs);
+        a.assertTrue(lhs + NOTSAME + rhs, lhs.equals(rhs));
+    }
+    
+    /** @return true if both are empty (null or no entries) or if all match */
+    public static void sameRA(String[] lhs, String[] rhs, Assert a) {        
+        if (null == lhs) {
+            a.assertTrue((null == rhs) || (0 == rhs.length));
+        } else if (null == rhs) {
+            a.assertTrue(0 == lhs.length);
+        } else {
+            String l = normal(lhs);
+            String r = normal(rhs);
+            a.assertTrue(l + NOTSAME + r, l.equals(r));
+        }
+    }
+        
+    /** @return normal form for String[] items*/
+    static String normal(String[] items) {
+        return (null == items ? "[]" : normal(Arrays.asList(items)));
+    }
+    
+    /** @return normal form for list items */
+    static String normal(List list) {
+        StringBuffer sb = new StringBuffer();
+        sb.append("[");
+        boolean first = true;
+        for (Iterator iter = list.iterator(); iter.hasNext();) {
+            Object o = iter.next();            
+            if (!first) {
+                sb.append(", ");
+            } else {
+                first = false;
+            }
+            sb.append(normal(o));
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+        
+    /** @return true if both are empty (null or no entries) or if all match after trimming */
+    public static void sameListSize(List lhs, List rhs, Assert a) {
+        if (null == lhs) {
+            a.assertTrue((null == rhs) || (0 == rhs.size()));
+        } else if (null == rhs) {
+            a.assertTrue(0 == lhs.size());
+        } else {
+            a.assertTrue(rhs.size() == lhs.size());
+        }        
+    }
+
+    /** @return true if both are empty (null or no entries) or if all match after trimming */
+    public static void sameList(List lhs, List rhs, Assert a) {
+        sameListSize(lhs, rhs, a);
+        String l = normal(lhs);
+        String r = normal(rhs);
+        String label = l + NOTSAME + r;
+        a.assertTrue(label, l.equals(r));
+    }
+    
+//    /**
+//     * Normalize and compare:
+//     * <li>bug id's are not compared since extracted during xml writing</li>
+//     * <li>keyword compare is disabled since keywords are generated during xml reading.</li> 
+//     * <li>description compare is normalized by stripping bug ids</li>
+//     * <li>String and arrays are equal when empty (null or 0-length)</li>
+//     * @see Ajctest#stripBugId(String)
+//     */
+//    public static void sameAjcTest(AjcTest lhs, AjcTest rhs, Assert reporter) {
+//        Assert a = reporter;
+//        String label = lhs + NOTSAME + rhs;
+//        a.assertTrue(label, null != lhs);
+//        a.assertTrue(label, null != rhs);
+//        //a.assertTrue(label, lhs.ignoreWarnings == rhs.ignoreWarnings);
+//        // XXX disabled - not in .txt 
+//        // sameStringList(lhs.keywords, rhs.keywords, a);
+//        // sameString(lhs.bugId, rhs.bugId, a);
+//        // argh - bugid stripped from description
+//        //same(AjcTest.stripBugId(lhs.description), AjcTest.stripBugId(lhs.description), a);
+//        //sameRA(lhs.globals, rhs.globals, a);
+//        //lhs.reset();
+//        //rhs.reset();
+//        boolean gotOne = false;
+//        IMessageHolder holder = new MessageHandler();
+//        a.assertTrue(label, !holder.hasAnyMessage(IMessage.FAIL, IMessageHolder.ORGREATER));
+//        while (lhs.hasNextRun() && rhs.hasNextRun()) {
+//            sameIAjcRun((IAjcRun) lhs.nextRun(holder), (IAjcRun) rhs.nextRun(holder), reporter);
+//            a.assertTrue(label, !holder.hasAnyMessage(IMessage.FAIL, IMessageHolder.ORGREATER));
+//            if (!gotOne) {
+//                gotOne = true;
+//            }
+//        }
+//        a.assertTrue(label, gotOne);
+//        a.assertTrue(label, !lhs.hasNextRun());
+//        a.assertTrue(label, !rhs.hasNextRun());
+//    }
+
+       public static void sameIAjcRun(IAjcRun lhs, IAjcRun rhs, Assert reporter) {
+        Assert a = reporter;
+        a.assertTrue(lhs != null);
+        a.assertTrue(rhs != null);
+        Class c = lhs.getClass();
+        a.assertTrue(c == rhs.getClass());
+        AbstractRunSpec lhsSpec;
+        AbstractRunSpec rhsSpec;
+        
+        if (c == CompilerRun.class) {
+            CompilerRun.Spec l = ((CompilerRun) lhs).spec;
+            CompilerRun.Spec r = ((CompilerRun) rhs).spec;
+            lhsSpec = l;
+            rhsSpec = r;
+            a.assertEquals(l.testSrcDirOffset, r.testSrcDirOffset);
+            a.assertEquals(l.compiler, r.compiler);
+        } else if (c == JavaRun.class) {
+            JavaRun.Spec l = ((JavaRun) lhs).spec;
+            JavaRun.Spec r = ((JavaRun) rhs).spec;
+            lhsSpec = l;
+            rhsSpec = r;
+            a.assertTrue(l.skipTester ==  r.skipTester);
+            a.assertEquals(l.className, r.className);
+        } else if (c == IncCompilerRun.class) {
+            IncCompilerRun.Spec l = ((IncCompilerRun) lhs).spec;
+            IncCompilerRun.Spec r = ((IncCompilerRun) rhs).spec;
+            lhsSpec = l;
+            rhsSpec = r;
+        } else {
+            assertTrue(lhs.equals(rhs));
+            return;
+        }
+        sameSpec(lhsSpec, rhsSpec, reporter);
+       }
+
+       public static void sameSpec(AbstractRunSpec lhs, AbstractRunSpec rhs, Assert a) {
+        if ((null == lhs) && (null == rhs)) {
+            return;
+        }
+        a.assertTrue(lhs != null);
+        a.assertTrue(rhs != null);
+        a.assertEquals(""+lhs.getOptionsList(), ""+rhs.getOptionsList());
+        sameList(lhs.getPathsList(), rhs.getPathsList(), a);
+        sameMessages(lhs.getMessages(), rhs.getMessages(), a);
+        sameDirChangesList(lhs.dirChanges, rhs.dirChanges, a);
+       }
+
+       public static void sameDirChangesList(ArrayList lhs, ArrayList rhs, Assert a) {
+        if ((null == lhs) && (null == rhs)) {
+            return;
+        }
+        a.assertTrue(rhs != null);
+        a.assertTrue(lhs != null);
+        sameListSize(lhs, rhs, a);
+        Iterator lhsIter = lhs.iterator();
+        Iterator rhsIter = rhs.iterator();
+        while (lhsIter.hasNext() && rhsIter.hasNext()) {
+            sameDirChangesSpec((DirChanges.Spec) lhsIter.next(), (DirChanges.Spec) rhsIter.next(), a);
+        }
+       }
+
+    public static void sameDirChangesSpec(DirChanges.Spec lhs, DirChanges.Spec rhs, Assert a) {
+        if ((null == lhs) && (null == rhs)) {
+            return;
+        }
+        a.assertTrue(rhs != null);
+        a.assertTrue(lhs != null);
+        a.assertEquals(lhs.defaultSuffix, rhs.defaultSuffix);        
+        a.assertEquals(lhs.dirToken, rhs.dirToken);
+        sameList(lhs.updated, rhs.updated, a);
+        sameList(lhs.removed, rhs.removed, a);
+        sameList(lhs.added, rhs.added, a);
+    }
+
+    public static void sameMessages(List one, List two, Assert a) {
+        if ((null == one) && (null == two)) {
+            return;
+        }
+        Iterator lhs = one.iterator();
+        Iterator rhs = two.iterator();
+        while (lhs.hasNext() && rhs.hasNext()) {
+            sameMessage((IMessage) lhs.next(), (IMessage) rhs.next(), a);
+        }
+        assertTrue(!lhs.hasNext());        
+        assertTrue(!rhs.hasNext());        
+    }
+
+    public static void sameMessage(IMessage lhs, IMessage rhs, Assert a) {
+        if ((null == lhs) && (null == rhs)) {
+            return;
+        }
+        a.assertTrue(lhs != null);
+        a.assertTrue(rhs != null);
+        a.assertTrue(lhs.getKind() == rhs.getKind());
+        same(lhs.getMessage(), rhs.getMessage(), a);
+        a.assertEquals(lhs.getThrown(), rhs.getThrown());
+        sameSourceLocation(lhs.getISourceLocation(), rhs.getISourceLocation(), a);        
+       }
+
+       public static void sameSourceLocation(ISourceLocation lhs, ISourceLocation rhs, Assert a) {
+        if ((null == lhs) && (null == rhs)) {
+            return;
+        }
+        a.assertTrue(lhs != null);
+        a.assertTrue(rhs != null);
+        a.assertTrue(lhs.getLine() == rhs.getLine());
+        a.assertTrue(lhs.getColumn() == rhs.getColumn());
+        a.assertTrue(lhs.getEndLine() == rhs.getEndLine());
+        // XXX need to compare files, permitting null == NONE        
+       }
+
+       /**
+        * Constructor for AjcSpecTest.
+        * @param name
+        */
+       public AjcSpecTest(String name) {
+               super(name);
+       }
+    
+    public void testMinimal() {
+        AjcTest.Spec one = new AjcTest.Spec();
+        AjcTest.Spec two = new AjcTest.Spec();
+        // empty/identity tests
+        sameAjcTestSpec(one, two, this);
+        
+        one.addOption("-one");
+        one.addKeyword("keyword");
+        one.addPath("path");
+        IMessage m = MessageUtil.info("info message");
+        one.addMessage(m);
+        DirChanges.Spec dcspec = new DirChanges.Spec();
+        dcspec.setDirToken("dirToken");
+        dcspec.setDefaultSuffix(".suffix");
+        one.addDirChanges(dcspec);
+
+        // full/identity tests
+        sameAjcTestSpec(one, one, this);
+        // XXX need to clone...
+
+        // XXX need to test that more differences are detected
+        boolean passed = false;
+        try {
+            sameAjcTestSpec(one, two, this);        
+        } catch (AssertionFailedError e) {
+            passed = true;
+        }
+        assertTrue("did not get expected exception", passed);
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/harness/bridge/CompilerRunSpecTest.java b/testing/testsrc/org/aspectj/testing/harness/bridge/CompilerRunSpecTest.java
new file mode 100644 (file)
index 0000000..ddd908b
--- /dev/null
@@ -0,0 +1,163 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.MessageHandler;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class CompilerRunSpecTest extends TestCase {
+
+       /**
+        * Constructor for CompilerRunSpecTest.
+        * @param name
+        */
+       public CompilerRunSpecTest(String name) {
+               super(name);
+       }
+
+    public void testSetupArgs() {
+        checkSetupArgs("verbose", false);
+        checkSetupArgs("lenient", false);
+        checkSetupArgs("strict", false);
+        checkSetupArgs("ajc", true);   // XXX need to predict/test compiler selection
+        checkSetupArgs("eclipse", true);
+    }
+
+    void checkSetupArgs(String arg, boolean isTestArg) {
+        MessageHandler handler = new MessageHandler();
+        CompilerRun.Spec spec = new CompilerRun.Spec();
+        AbstractRunSpec.RT parentRuntime = new AbstractRunSpec.RT();
+        String result;
+        String expResult;
+        
+        // -------- local set
+        // global ^ (force-off) to disable
+        spec.setOptions("-" + arg);
+        parentRuntime.setOptions(new String[] {"^" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        assertTrue(result, "[]".equals(result));
+
+        // global ! (force-on) does not change local-set
+        parentRuntime.setOptions(new String[] {"!" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+        assertTrue(result, expResult.equals(result));
+
+        // global - (set) does not change local-set
+        parentRuntime.setOptions(new String[] {"-" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+        assertTrue(result, expResult.equals(result));
+
+        // global (unset) does not change local-set
+        parentRuntime.setOptions(new String[] {""});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+        assertTrue(result, expResult.equals(result));
+
+        // -------- local force-on
+        // global ^ (force-off) conflicts with local force-on
+        spec.setOptions("!" + arg);
+        parentRuntime.setOptions(new String[] {"^" + arg});
+        assertTrue(!spec.adoptParentValues(parentRuntime, handler));
+        assertTrue(0 != handler.numMessages(null, true));
+        handler.init();
+
+        // global ! (force-on) does not change local force-on
+        parentRuntime.setOptions(new String[] {"!" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+        assertTrue(result, expResult.equals(result));
+
+        // global - (set) does not change local force-on
+        parentRuntime.setOptions(new String[] {"-" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+        assertTrue(result, expResult.equals(result));
+
+        // global (unset) does not change local force-on
+        parentRuntime.setOptions(new String[] {""});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+        assertTrue(result, expResult.equals(result));
+
+
+        // -------- local force-off
+        // global ^ (force-off) does not change local force-off
+        spec.setOptions("^" + arg);
+        parentRuntime.setOptions(new String[] {"^" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        assertTrue(result, ("[]").equals(result));
+
+        // global ! (force-on) conflicts with local force-off
+        parentRuntime.setOptions(new String[] {"!" + arg});
+        assertTrue(!spec.adoptParentValues(parentRuntime, handler));
+        assertTrue(0 != handler.numMessages(null, true));
+        handler.init();
+
+        // global - (set) overridden by local force-off // XXX??
+        parentRuntime.setOptions(new String[] {"-" + arg});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        assertTrue(result, ("[]").equals(result));
+
+        // global (unset) does not change local force-off
+        parentRuntime.setOptions(new String[] {""});
+        assertTrue(spec.adoptParentValues(parentRuntime, handler));
+        if (0 != handler.numMessages(null, true)) {
+            assertTrue(handler.toString(), false);
+        }
+        result = ""+spec.testSetup.commandOptions;
+        assertTrue(result, ("[]").equals(result));
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/harness/bridge/ParseTestCase.java b/testing/testsrc/org/aspectj/testing/harness/bridge/ParseTestCase.java
new file mode 100644 (file)
index 0000000..c8a708e
--- /dev/null
@@ -0,0 +1,228 @@
+/* *******************************************************************
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.harness.bridge;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.IMessageHandler;
+import org.aspectj.bridge.IMessageHolder;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.Message;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.bridge.SourceLocation;
+import org.aspectj.testing.run.IRun;
+import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.run.IRunListener;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.IRunValidator;
+import org.aspectj.testing.run.RunStatus;
+import org.aspectj.testing.run.RunValidator;
+import org.aspectj.testing.run.Runner;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import junit.framework.TestCase;
+
+public class ParseTestCase extends TestCase {
+
+       public ParseTestCase(String name) {
+               super(name);
+       }
+       
+       
+       public void testParse() throws Exception { // XXX failing b/c of iteration
+               Runner runner = new Runner();
+        IMessageHolder handler = new MessageHandler();
+               RunStatus status;
+        Validator validator = new Validator(handler);
+        final File suiteFile = new File("../testing/testdata/suite.xml");
+               List tests = parseSuite(suiteFile);
+        Sandbox sandbox = new Sandbox(new File("testdata"), validator);                
+        IRunListener listenerNULL = null;              
+        ISourceLocation sl = new SourceLocation(suiteFile, 0, 0,0);
+               for (Iterator iter = tests.iterator(); iter.hasNext();) {
+            status = new RunStatus(handler, runner);
+            AjcTest.Spec test = (AjcTest.Spec) iter.next();
+            test.setSourceLocation(sl);
+                       IRunIterator child = test.makeRunIterator(sandbox, validator);
+            //test.setup(new String[0], validator); // XXX
+                       //IRun child = runner.wrap(test, null);
+            // huh? runIterator not generating child status?
+            //RunStatus childStatus = runner.makeChildStatus();
+            runner.runIterator(child, status, listenerNULL); 
+            MessageUtil.print(System.err, status);
+               }
+       }
+
+       private List parseSuite(File file) throws ParserConfigurationException, IOException, SAXException{
+               DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+               factory.setValidating(true);
+               factory.setIgnoringElementContentWhitespace(true);
+               factory.setIgnoringComments(true);
+               
+
+               DocumentBuilder builder = factory.newDocumentBuilder();
+               System.out.println(file.getAbsoluteFile());
+               Document doc = builder.parse(file);
+
+               dump(doc.getDocumentElement(), 0);
+               
+               List ret = new ArrayList();
+               Node suiteNode = doc.getDocumentElement();
+               
+               NodeList children = suiteNode.getChildNodes();
+               for (int i=0; i < children.getLength(); i++) {
+                       ret.add(parseTest(children.item(i)));
+               }
+               
+               return ret;
+       }
+
+       private AjcTest.Spec parseTest(Node node) {
+               String title = getAttributeString(node, "title");
+               String pr = getAttributeString(node, "pr");
+               String dir = getAttributeString(node, "dir");
+               
+               ISourceLocation sourceLocation =
+                   new SourceLocation(new File("Missing"), 0, 0, 0);
+        AjcTest.Spec test = new AjcTest.Spec();
+        test.setDescription(title);
+        test.setTestDirOffset(dir);
+        test.setBugId(Integer.valueOf(pr).intValue());
+        test.setSourceLocation(sourceLocation);
+               //AjcTest test = new AjcTest(title, dir, pr, sourceLocation);
+               
+               System.out.println(test);
+               
+               List ret = new ArrayList();
+               
+               NodeList children = node.getChildNodes();
+               for (int i=0; i < children.getLength(); i++) {
+            test.addChild(parseIRun(test, children.item(i), dir));
+//                     test.addRunSpec(parseIRun(test, children.item(i), dir));
+               }
+               
+               return test;
+       }
+
+       private IRunSpec parseIRun(AjcTest.Spec test, Node node, String baseDir) {
+               String kind = node.getNodeName();
+               if (kind.equals("compile")) {
+                       List args = parseChildrenStrings(node, "arg");
+                       List files = parseChildrenStrings(node, "file");
+                       List expectedMessages = parseChildrenMessages(node);
+            CompilerRun.Spec spec = new CompilerRun.Spec();
+            spec.addOptions((String[]) args.toArray(new String[0]));
+            spec.addPaths((String[]) args.toArray(new String[0]));
+            spec.addMessages(expectedMessages);
+            spec.testSrcDirOffset = null; // baseDir; 
+            return spec;
+               } else if (kind.equals("run")) {
+            JavaRun.Spec spec = new JavaRun.Spec();
+            spec.className = getAttributeString(node, "class");
+            spec.addOptions(new String[0]);  //??? could add support here
+            JavaRun run = new JavaRun(spec);
+                       return spec;
+               }
+               
+               return null;
+       }
+
+       private List parseChildrenMessages(Node node) {
+               List ret = new ArrayList();
+               
+               NodeList children = node.getChildNodes();
+               for (int i=0; i < children.getLength(); i++) {
+                       Node child = children.item(i);
+                       if (child.getNodeName().equals("message")) {
+                               ret.add(parseMessage(child));
+                       }
+               }
+               return ret;
+       }
+
+       private IMessage parseMessage(Node child) {
+               IMessage.Kind kind;
+               String sKind = getAttributeString(child, "kind");
+               if (sKind.equals("error")) { kind = IMessage.ERROR; }
+               else if (sKind.equals("warning")) { kind = IMessage.WARNING; }
+               else {
+                       throw new RuntimeException("unknown kind: " + sKind);
+               }
+               String filename = getAttributeString(child, "file");
+               File file;
+               if (filename != null) {
+                       file = new File(filename);
+               } else {
+                       file = new File("XXX");  //XXX 
+               }
+               
+               int line = Integer.valueOf(getAttributeString(child, "line")).intValue();
+               
+               ISourceLocation sourceLocation = new SourceLocation(file, line, line, 0);
+               
+               return new Message("", kind, null, sourceLocation);
+       }
+
+
+
+       private List parseChildrenStrings(Node node, String kind) {
+               List ret = new ArrayList();
+               
+               NodeList children = node.getChildNodes();
+               for (int i=0; i < children.getLength(); i++) {
+                       Node child = children.item(i);
+                       if (child.getNodeName().equals(kind)) {
+                Node first = child.getFirstChild();
+                if (null != first) {
+                    ret.add(first.getNodeValue());// XXX
+                }
+                       }
+               }
+               return ret;
+       }
+
+
+
+       private String getAttributeString(Node node, String name) {
+               Node attrNode = node.getAttributes().getNamedItem(name);
+               if (attrNode == null) return null;
+               return attrNode.getNodeValue();
+       }
+
+
+
+
+       private void dump(Node node, int indent) {
+               for (int i=0; i < indent; i++) System.out.print("  ");
+               System.out.println(node);
+               NodeList children = node.getChildNodes();
+               for (int i=0; i < children.getLength(); i++) {
+                       dump(children.item(i), indent+1);
+               }
+       }
+
+
+}
diff --git a/testing/testsrc/org/aspectj/testing/harness/bridge/TestingBridgeTests.java b/testing/testsrc/org/aspectj/testing/harness/bridge/TestingBridgeTests.java
new file mode 100644 (file)
index 0000000..05d64b0
--- /dev/null
@@ -0,0 +1,32 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.harness.bridge;
+
+import junit.framework.*;
+
+public class TestingBridgeTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(TestingBridgeTests.class.getName());
+        //$JUnit-BEGIN$
+        suite.addTestSuite(AjcSpecTest.class); 
+        suite.addTestSuite(ParseTestCase.class); 
+        //$JUnit-END$
+        return suite;
+    }
+
+    public TestingBridgeTests(String name) { super(name); }
+
+}  
diff --git a/testing/testsrc/org/aspectj/testing/util/BridgeUtilTest.java b/testing/testsrc/org/aspectj/testing/util/BridgeUtilTest.java
new file mode 100644 (file)
index 0000000..ae0a7cd
--- /dev/null
@@ -0,0 +1,100 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.run.IRun;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.RunStatus;
+import org.aspectj.testing.run.Runner;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class BridgeUtilTest extends TestCase {
+
+       public BridgeUtilTest(String name) {
+               super(name);
+       }
+
+    public void testChildString() {
+        String expect;
+        String id;
+        id = "run status identifier";
+        expect  = "PASS " + id + " 0 tests";
+        checkChildString(id, 0, 0, 0, 0, expect);
+        expect  = "PASS " + id + " 2 tests (2 skipped)";
+        checkChildString(id, 2, 0, 0, 0, expect);
+        expect  = "PASS " + id + " 3 tests (1 skipped, 2 passed)";
+        checkChildString(id, 1, 0, 0, 2, expect);
+        expect  = "FAIL " + id + " 3 tests (1 skipped, 2 failed)";
+        checkChildString(id, 1, 0, 2, 0, expect);
+        expect  = "FAIL " + id + " 6 tests (1 skipped, 2 failed, 3 passed)";
+        checkChildString(id, 1, 0, 2, 3, expect);
+        expect  = "FAIL " + id + " 1 tests (1 failed)";
+        checkChildString(id, 0, 0, 1, 0, expect);
+        expect  = "FAIL " + id + " 4 tests (1 failed, 3 passed)";
+        checkChildString(id, 0, 0, 1, 3, expect);
+        expect  = "PASS " + id + " 1 tests (1 passed)";
+        checkChildString(id, 0, 0, 0, 1, expect);
+        
+        // "incomplete" variants
+        expect  = "PASS " + id + " 5 tests (5 incomplete)";
+        checkChildString(id, 0, 5, 0, 0, expect);
+        expect  = "PASS " + id + " 7 tests (2 skipped, 5 incomplete)";
+        checkChildString(id, 2, 5, 0, 0, expect);
+        expect  = "PASS " + id + " 8 tests (1 skipped, 5 incomplete, 2 passed)";
+        checkChildString(id, 1, 5, 0, 2, expect);
+        expect  = "FAIL " + id + " 8 tests (1 skipped, 5 incomplete, 2 failed)";
+        checkChildString(id, 1, 5, 2, 0, expect);
+        expect  = "FAIL " + id + " 11 tests (1 skipped, 5 incomplete, 2 failed, 3 passed)";
+        checkChildString(id, 1, 5, 2, 3, expect);
+        expect  = "FAIL " + id + " 6 tests (5 incomplete, 1 failed)";
+        checkChildString(id, 0, 5, 1, 0, expect);
+        expect  = "FAIL " + id + " 9 tests (5 incomplete, 1 failed, 3 passed)";
+        checkChildString(id, 0, 5, 1, 3, expect);
+        expect  = "PASS " + id + " 6 tests (5 incomplete, 1 passed)";
+        checkChildString(id, 0, 5, 0, 1, expect);
+    }
+    
+    void checkChildString(String id, int numSkips, int numIncomplete, int numFails, int numPasses, 
+                        String expected) {
+        Runner runner = new Runner();
+        MessageHandler holder = new MessageHandler();
+        RunStatus status = new RunStatus(holder, runner);
+        status.setIdentifier(id);
+        status.start();
+
+        final IRun failer = new IRun() {
+            public boolean run(IRunStatus status) { return false; }
+        };
+        final IRun passer = new IRun() {
+            public boolean run(IRunStatus status) { return true; }
+        };
+        final Object result = (numFails > 0 ? IRunStatus.FAIL : IRunStatus.PASS);
+        while (numFails-- > 0) {
+            runner.runChild(failer,status, null, null);         
+        }
+        while (numPasses-- > 0) {
+            runner.runChild(passer,status, null, null);         
+        }
+        status.finish(result);
+        String actual = BridgeUtil.childString(status, numSkips, numIncomplete);
+        String label = " expected \"" + expected + "\" got \"" + actual + "\"";
+        assertTrue(label, expected.equals(actual));
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/FileUtilTest.java b/testing/testsrc/org/aspectj/testing/util/FileUtilTest.java
new file mode 100644 (file)
index 0000000..1ff4d6c
--- /dev/null
@@ -0,0 +1,46 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class FileUtilTest extends TestCase {
+
+       /**
+        * Constructor for FileUtilTest.
+        * @param arg0
+        */
+       public FileUtilTest(String arg0) {
+               super(arg0);
+       }
+
+    public void testFileEquals() {
+        // File.equals(..) is based on lexical compare of filenames
+//        File rf = new File("testsrc/org/aspectj/testing/util/FileUtilTest.java");
+//        File rb = new File("testsrc\\org\\aspectj\\testing\\util\\FileUtilTest.java");
+//        String a = rf.getAbsolutePath().replace('\\', '/');
+//        File af = new File(a);
+//        File ab = new File(a.replace('/', '\\'));
+//        list.add(af);
+//        list.add(ab);
+//        list.add(rb);
+//        assertTrue(list.contains(duplicateTwo));
+//        assertTrue(list.contains(anotherOne));
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/IteratorWrapperTest.java b/testing/testsrc/org/aspectj/testing/util/IteratorWrapperTest.java
new file mode 100644 (file)
index 0000000..fdcd695
--- /dev/null
@@ -0,0 +1,164 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class IteratorWrapperTest extends TestCase {
+
+       /**
+        * Constructor for IteratorWrapperTest.
+        * @param name
+        */
+       public IteratorWrapperTest(String name) {
+               super(name);
+       }
+
+       public void testIteratorWrapper() {
+        Object[][] exp = new Object[][] {};
+        List[] in = new List[] {};
+        checkIteratorWrapper(in, exp);
+
+        in = new List[] {Collections.EMPTY_LIST};
+        checkIteratorWrapper(in, exp);
+        
+        in = new List[] {Collections.EMPTY_LIST, Collections.EMPTY_LIST};
+        checkIteratorWrapper(in, exp);
+        
+        Object[] ra1 = new Object[] { "1" };
+        List one = Collections.unmodifiableList(Arrays.asList(ra1));
+        in = new List[] {one};
+        exp = new Object[][] { ra1 };
+        checkIteratorWrapper(in, exp);
+
+        in = new List[] {one, one};
+        exp = new Object[][] { new Object[] { "1", "1"} };
+        checkIteratorWrapper(in, exp);
+
+        Object[] RA_ab = new String[] { "a", "b" };
+        List List_ab = Collections.unmodifiableList(Arrays.asList(RA_ab));
+        in = new List[] {List_ab};
+        exp = new Object[][] { 
+            new Object[] { "a" }, 
+            new Object[] { "b" } 
+            };
+        checkIteratorWrapper(in, exp);
+
+        in = new List[] {one, List_ab};
+        exp = new Object[][] { 
+            new Object[] { "1", "a" }, 
+            new Object[] { "1", "b" },
+            };
+        checkIteratorWrapper(in, exp);
+
+        Object[] RA_cd = new String[] { "c", "d" };
+        List List_cd = Collections.unmodifiableList(Arrays.asList(RA_cd));
+        
+        in = new List[] {List_ab, List_cd};
+        exp = new Object[][] { 
+            new Object[] { "a", "c" }, 
+            new Object[] { "b", "c" }, 
+            new Object[] { "a", "d" }, 
+            new Object[] { "b", "d" }
+            };
+        checkIteratorWrapper(in, exp);
+        
+        in = new List[] {one, one, one};
+        exp = new Object[][] { 
+            new Object[] { "1", "1", "1" } 
+            };
+        checkIteratorWrapper(in, exp);
+        
+        in = new List[] {List_ab, List_ab, List_ab};
+        exp = new Object[][] { 
+            new Object[] { "a", "a", "a" }, 
+            new Object[] { "b", "a", "a" }, 
+            new Object[] { "a", "b", "a" }, 
+            new Object[] { "b", "b", "a" }, 
+            new Object[] { "a", "a", "b" },
+            new Object[] { "b", "a", "b" },
+            new Object[] { "a", "b", "b" },
+            new Object[] { "b", "b", "b" }
+            };
+        checkIteratorWrapper(in, exp);
+        
+        in = new List[] {one, List_ab, List_ab};
+        exp = new Object[][] { 
+            new Object[] { "1", "a", "a" }, 
+            new Object[] { "1", "b", "a" },
+            new Object[] { "1", "a", "b" }, 
+            new Object[] { "1", "b", "b" },
+            };
+        checkIteratorWrapper(in, exp);       
+        
+        in = new List[] {one, List_ab, one};
+        exp = new Object[][] { 
+            new Object[] { "1", "a", "1" }, 
+            new Object[] { "1", "b", "1" }
+            };
+        checkIteratorWrapper(in, exp);
+        
+        in = new List[] {List_ab, one, List_ab};
+        exp = new Object[][] { 
+            new Object[] { "a", "1", "a" }, 
+            new Object[] { "b", "1", "a" },
+            new Object[] { "a", "1", "b" }, 
+            new Object[] { "b", "1", "b" }
+            };
+        checkIteratorWrapper(in, exp);
+        
+        in = new List[] {List_ab, one, List_ab, List_ab, Collections.EMPTY_LIST};
+        exp = new Object[][] {};
+        checkIteratorWrapper(in, exp);
+        
+    }
+
+    void checkIteratorWrapper(List[] lists, Object[][] exp) {
+        IteratorWrapper it = new IteratorWrapper(lists);
+        for (int i = 0; i < exp.length; i++) {
+                       Object[] e = exp[i];
+            if (!it.hasNext()) {
+                String s = "exp[" + i + "]: " + Arrays.asList(e) + " it=" + it;
+                assertTrue(s, false);
+            }
+            Object[] actual = (Object[]) it.next();
+            checkEquals(e, actual, i);
+               }
+        if (it.hasNext()) {
+            String s = "> " + exp.length + " it=" + it;
+            assertTrue(s, false);
+        }
+    }
+
+    void checkEquals(Object[] exp, Object[] actual, int index) {
+        if (null == exp) {
+            assertTrue(null == actual);
+        } else {
+            assertTrue(null != actual);
+        }
+        String s = "] exp=" + Arrays.asList(exp) + " act=" + Arrays.asList(actual);
+        assertTrue(s, exp.length == actual.length);
+        for (int i = 0; i < actual.length; i++) {
+                       assertTrue(null != exp[i]);
+            assertTrue("[" + index + ", " + i + s, exp[i].equals(actual[i]));
+               }
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/LangUtilTest.java b/testing/testsrc/org/aspectj/testing/util/LangUtilTest.java
new file mode 100644 (file)
index 0000000..ebdb21c
--- /dev/null
@@ -0,0 +1,342 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import junit.framework.TestCase;
+import junit.textui.TestRunner;
+
+/**
+ * 
+ * @author isberg
+ */
+public class LangUtilTest extends TestCase {
+
+    private static final String ME 
+        = "org.aspectj.testing.util.LangUtilTest";
+
+    /** @param args ignored */
+    public static void main(String[] args) {
+        TestRunner.main(new String[] {ME});
+    }
+    
+       /**
+        * Constructor for LangUtilTest.
+        * @param name
+        */
+       public LangUtilTest(String name) {
+               super(name);
+       }
+
+       void check(String l, StringTokenizer st, int max, String delim) {
+        for (int i = 0; i < max; i++) {
+            if ((i > 0) && (null != delim)) {
+                assertEquals(l, delim, st.nextToken());
+            }
+            assertEquals(l, ""+i, st.nextToken());
+               }
+        assertTrue(l, !st.hasMoreTokens());
+    }
+    
+
+       void checkUnflatten(FTest test) {
+        String[] exp = test.unflattened;
+        ArrayList result = LangUtil.unflatten(test.toUnflatten, test.spec);
+        String label = test + " -> " + result;
+        assertNotNull(label, result);
+        
+        assertEquals(label, exp.length, result.size()); 
+        for (int i = 0; i < exp.length; i++) {
+                       assertEquals(label, exp[i], result.get(i));
+               }
+    }
+    
+
+       public void skiptestUnflatten() {
+        LangUtil.FlattenSpec COMMA = LangUtil.FlattenSpec.COMMA;
+        LangUtil.FlattenSpec LIST = LangUtil.FlattenSpec.LIST;
+            
+        FTest[] tests = new FTest[]
+        { new FTest("[]", new String[0], LIST)
+        , new FTest("[1]", new String[] {"1"}, LIST)
+        , new FTest("[1, 2]", new String[] {"1", "2"}, LIST)
+        , new FTest("[1,2]", new String[] {"1,2"}, LIST)
+        , new FTest("[1, 2, 3]", new String[] {"1","2","3"}, LIST)        
+        };
+        for (int i = 0; i < tests.length; i++) {
+                       checkUnflatten(tests[i]);
+               }
+    }
+    
+
+       public void testArrayList() {
+        ArrayList l = new ArrayList();
+        l.add(null);
+        l.add(null);
+        assertTrue(null == l.get(0));
+        assertTrue(null == l.get(1));
+        assertTrue(2 == l.size());
+        assertEquals("[null, null]", "" + l);
+    }
+    
+    public void testCombineStrings() {
+        String[] one = new String[]{};
+        String[] two = new String[]{};
+        String[] expect = new String[]{};
+        checkCombineStrings(one, two, expect);
+        
+        one = new String[]{null};
+        two = new String[]{null};
+        expect = new String[]{};
+        checkCombineStrings(one, two, expect);
+        
+        one = new String[]{"1"};
+        two = new String[]{null};
+        expect = new String[]{"1"};
+        checkCombineStrings(one, two, expect);
+        
+        one = new String[]{null};
+        two = new String[]{"2"};
+        expect = new String[]{"2"};
+        checkCombineStrings(one, two, expect);
+        
+        one = new String[]{"1"};
+        two = new String[]{"2"};
+        expect = new String[]{"1", "2"};
+        checkCombineStrings(one, two, expect);
+        
+        one = new String[]{null, null, "1", null, null};
+        two = new String[]{null, "2", null};
+        expect = new String[]{"1", "2"};
+        checkCombineStrings(one, two, expect);
+
+        one = new String[]{"1", "2", "3", "4"};
+        two = new String[]{"5", null, "6"};
+        expect = new String[]{"1", "2", "3", "4", "5", "6"};
+        checkCombineStrings(one, two, expect);
+        
+    }
+    void checkCombineStrings(String[] one, String[] two, String[] expected) {
+        String[] actual = LangUtil.combine(one, two);
+        String aString = LangUtil.arrayAsList(actual).toString();
+        String eString = LangUtil.arrayAsList(expected).toString();
+        String both = "actual=\"" + aString + "\" expected=\"" + eString + "\"";
+        assertTrue(both, aString.equals(eString));
+    }
+    
+    final String[] sABCDE     = new String[] {"A", "B", "C", "D", "E" };
+    final String[] sABC       = new String[] {"A", "B", "C" };
+    final String[] sDE        = new String[] {"D", "E" };
+    final String[] sabcde     = new String[] {"a", "b", "c", "d", "e" };
+    final String[] sabc       = new String[] {"a", "b", "c" };
+    final String[] sde        = new String[] {"d", "e" };
+    final String[] s12345     = new String[] {"1", "2", "3", "4", "5" };
+    final String[] s13579     = new String[] {"1", "3", "5", "7", "9" };
+    final String[] s02468     = new String[] {"0", "2", "4", "6", "8" };
+    final String[] s135       = new String[] {"1", "3", "5" };
+    final String[] s79        = new String[] {"7", "9" };
+    final String[] s24        = new String[] {"2", "4" };
+    final String[] s0         = new String[] {"0"};
+    final String[] s068       = new String[] {"0", "6", "8" };
+    final boolean unmodifiable = true;
+    final boolean modifiable  = false;
+    final List lABCDE         = makeList(unmodifiable, sABCDE);
+    final List lABC           = makeList(unmodifiable, sABC);
+    final List lDE            = makeList(unmodifiable, sDE);
+    final List labcde         = makeList(unmodifiable, sabcde);
+    final List labc           = makeList(unmodifiable, sabc);
+    final List lde            = makeList(unmodifiable, sde);
+    final List l12345         = makeList(unmodifiable, s12345);
+    final List l13579         = makeList(unmodifiable, s13579);
+    final List l02468         = makeList(unmodifiable, s02468);
+    final List l135           = makeList(unmodifiable, s135);
+    final List l79            = makeList(unmodifiable, s79);
+    final List l24            = makeList(unmodifiable, s24);
+    final List l0             = makeList(unmodifiable, s0);
+    final List l068           = makeList(unmodifiable, s068);
+    final List rlabcde        = makeList(modifiable, sabcde);
+    final List rlabc          = makeList(modifiable, sabc);
+    final List rlde           = makeList(modifiable, sde);
+    final List rlABCDE        = makeList(modifiable, sABCDE);
+    final List rlABC          = makeList(modifiable, sABC);
+    final List rlDE           = makeList(modifiable, sDE);
+    final List rl12345        = makeList(modifiable, s12345);
+    final List rl13579        = makeList(modifiable, s13579);
+    final List rl02468        = makeList(modifiable, s02468);
+    final List rl135          = makeList(modifiable, s135);
+    final List rl79           = makeList(modifiable, s79);
+    final List rl24           = makeList(modifiable, s24);
+    final List rl0            = makeList(modifiable, s0);
+    final List rl068          = makeList(modifiable, s068);
+    final List NONE = Collections.EMPTY_LIST;
+    {
+        Collections.shuffle(rlABCDE);
+        Collections.shuffle(rlABC);
+        Collections.shuffle(rlDE);
+        Collections.shuffle(rlabcde);
+        Collections.shuffle(rlabc);
+        Collections.shuffle(rlde);
+        Collections.shuffle(rl12345);
+        Collections.shuffle(rl13579);
+        Collections.shuffle(rl02468);
+        Collections.shuffle(rl135);
+        Collections.shuffle(rl79);
+        Collections.shuffle(rl24);
+        Collections.shuffle(rl0);
+        Collections.shuffle(rl068);        
+    }
+       
+   
+    public void testDiffsEmptyIdentities() {
+        checkDiff(l02468, null, l02468, NONE);
+        checkDiff(null, l02468, NONE, l02468);
+        checkDiff(l0, null, l0, NONE);
+        checkDiff(null, l0, NONE, l0);
+        checkDiff(l0, rl0, NONE, NONE);
+        checkDiff(labc, rlabc, NONE, NONE);
+    }
+
+    public void testDiffsEmpties() {
+        checkDiff(NONE, NONE, NONE, NONE);
+        checkDiff(null, NONE, NONE, NONE);
+        checkDiff(NONE, null, NONE, NONE);
+        checkDiff(null, null, NONE, NONE);
+        checkDiff(null, null, NONE, NONE);
+    }
+
+    public void testDiffsIdentities() {
+        checkDiff(l02468, l02468, NONE, NONE);
+        checkDiff(rl02468, l02468, NONE, NONE);
+        checkDiff(l02468, rl02468, NONE, NONE);
+        checkDiff(l13579, l13579, NONE, NONE);
+        checkDiff(rl13579, l13579, NONE, NONE);
+        checkDiff(l13579, rl13579, NONE, NONE);
+        checkDiff(l13579, rl13579, NONE, NONE);
+    }
+    public void testDiffsEvens() {
+        checkDiff(l02468, l12345, l068, l135);
+        checkDiff(rl02468, rl12345, rl068, rl135);
+    }
+    
+    public void testDiffsOdds() {
+        checkDiff(l13579, l12345, l79, l24);
+        checkDiff(rl13579, rl12345, rl79, rl24);
+        checkDiff(l13579, rl12345, l79, rl24);
+        checkDiff(rl13579, l12345, rl79, l24);
+    }
+    
+    public void testSoftDiffs() {
+        checkDiffSoft(labcde, lABCDE, NONE, NONE);
+        checkDiffSoft(lABC, labc, NONE, NONE);
+        checkDiffSoft(lABCDE, lABC, lDE, NONE);
+        checkDiffSoft(lDE, lABCDE, NONE, lABC);
+        checkDiffSoft(rlABCDE, rlABC, rlDE, NONE);
+        checkDiffSoft(rlDE, rlABCDE, NONE, rlABC);
+        checkDiffSoft(labcde, lABC, lDE, NONE);
+        checkDiffSoft(lde, lABCDE, NONE, lABC);
+        checkDiffSoft(rlabcde, rlABC, rlDE, NONE);
+        checkDiffSoft(rlde, rlABCDE, NONE, rlABC);
+    }
+
+    // ---------------------- utilities
+    List makeList(boolean unmodifiable, String[] ra) {
+        if (unmodifiable) {
+            return Collections.unmodifiableList(Arrays.asList(ra));
+        } else {
+            ArrayList list = new ArrayList();
+            list.addAll(Arrays.asList(ra));
+            return list;
+        }
+    }
+    
+    /** check both hard and soft - assuming list contain String */
+    void checkDiff(List expected, List actual, List missing, List extra) {
+        ArrayList extraOut = new ArrayList();
+        ArrayList missingOut = new ArrayList();
+        LangUtil.makeDiffs(expected, actual, missingOut, extraOut);
+        checkSame(missing, missingOut);
+        checkSame(extra, extraOut);
+        extraOut.clear();
+        missingOut.clear();
+        
+        LangUtil.makeSoftDiffs(expected, actual, missingOut, extraOut,
+                            String.CASE_INSENSITIVE_ORDER);
+        checkSame(missing, missingOut); // XXX does not detect bad order
+        checkSame(extra, extraOut);        
+    }
+    
+    void checkSame(Collection one, Collection two) { // just convert and string-compare?
+        String label = one + "?=" + two;
+        assertTrue(label, (null == one) == (null == two));
+        if (null != one) {
+            assertTrue(label, one.containsAll(two));
+            assertTrue(label, two.containsAll(one));
+        }
+    }
+
+    /** check only soft - assuming list contain String */
+    void checkDiffSoft(List expected, List actual, List missing, List extra) {
+        ArrayList extraOut = new ArrayList();
+        ArrayList missingOut = new ArrayList();
+        LangUtil.makeSoftDiffs(expected, actual, missingOut, extraOut,
+                            String.CASE_INSENSITIVE_ORDER);
+        checkSameSoft(missing, missingOut);
+        checkSameSoft(extra, extraOut);        
+    }
+
+    /** @param one modifiable List of String
+     * @param two modifiable List of String
+     */
+    void checkSameSoft(List one, List two) { // assume String
+        String label = one + "?=" + two;
+        assertTrue(label, (null == one) == (null == two));
+        if (null != one) {
+            ArrayList aone = new ArrayList();
+            aone.addAll(one);
+            ArrayList atwo = new ArrayList();
+            aone.addAll(two);
+            Collections.sort(aone);
+            Collections.sort(atwo);
+            String sone = (""+aone).toLowerCase();
+            String stwo = (""+aone).toLowerCase();
+            assertTrue(label, sone.equals(stwo));
+        }
+    }
+    
+    static class FTest {
+        String toUnflatten;
+        String[] unflattened;
+        LangUtil.FlattenSpec spec;
+        FTest(String in, String[] out, LangUtil.FlattenSpec spec) {
+            toUnflatten = in;
+            unflattened = out;
+            this.spec = spec;
+        }
+        public String toString() {
+            return "FTest(" 
+                + "toUnflatten=" + toUnflatten
+                + ", unflattened=" + Arrays.asList(unflattened)
+                + ", spec=" + spec
+                + ")";
+        }
+    }
+
+    
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/MessageUtilTest.java b/testing/testsrc/org/aspectj/testing/util/MessageUtilTest.java
new file mode 100644 (file)
index 0000000..26d8523
--- /dev/null
@@ -0,0 +1,130 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import org.aspectj.bridge.IMessage;
+import org.aspectj.bridge.ISourceLocation;
+import org.aspectj.bridge.Message;
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.bridge.SourceLocation;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class MessageUtilTest extends TestCase {
+    public MessageUtilTest(String s) {
+        super(s);
+    }
+    
+    MessageHandler samples;
+    List /* Exception */ exceptions;
+    List /*ISourceLocation*/ locations;
+    List /*String */ messageTexts;
+    
+
+    public void testMessageRendering() {
+        MessageHandler messages = getSampleMessages();
+        System.out.println("testMessageRendering(): run manually evaluate by inspection");
+        PrintStream oldOut = System.out;
+        // comment to inspect manually
+        System.setOut(NullPrintStream.NULL_PrintStream);
+        try {
+            MessageUtil.print(System.out, messages, "all             label -> ", MessageUtil.MESSAGE_LABEL, MessageUtil.PICK_ALL);
+            MessageUtil.print(System.out, messages, "info            short -> ", MessageUtil.MESSAGE_SHORT, MessageUtil.PICK_INFO);
+            MessageUtil.print(System.out, messages, "fail            line  -> ", MessageUtil.MESSAGE_LINE, MessageUtil.PICK_FAIL);
+            MessageUtil.print(System.out, messages, "debug      wide line  -> ", MessageUtil.MESSAGE_WIDELINE, MessageUtil.PICK_DEBUG);
+            MessageUtil.print(System.out, messages, "warn    no-loc label  -> ", MessageUtil.MESSAGE_LABEL_NOLOC, MessageUtil.PICK_WARNING);
+            MessageUtil.print(System.out, messages, "abort  force-loc line -> ", MessageUtil.MESSAGE_LINE_FORCE_LOC, MessageUtil.PICK_ABORT);
+            MessageUtil.print(System.out, messages, "info+           short -> ", MessageUtil.MESSAGE_SHORT, MessageUtil.PICK_INFO_PLUS);
+            MessageUtil.print(System.out, messages, "fail+           line  -> ", MessageUtil.MESSAGE_LINE, MessageUtil.PICK_FAIL_PLUS);
+            MessageUtil.print(System.out, messages, "debug+     wide line  -> ", MessageUtil.MESSAGE_WIDELINE, MessageUtil.PICK_DEBUG_PLUS);
+            MessageUtil.print(System.out, messages, "warn+   no-loc label  -> ", MessageUtil.MESSAGE_LABEL_NOLOC, MessageUtil.PICK_WARNING_PLUS);
+            MessageUtil.print(System.out, messages, "abort+ force-loc line -> ", MessageUtil.MESSAGE_LINE_FORCE_LOC, MessageUtil.PICK_ABORT_PLUS);
+        } finally {
+            System.setOut(oldOut);
+        }
+    }
+
+
+    List getSampleMessageTexts() {
+        if (null == messageTexts) {
+            ArrayList result = new ArrayList();
+            result.addAll(Arrays.asList(new String[]
+                { "one", "two", "now is the time for all good men..." }));
+            messageTexts = result;
+        }
+        return messageTexts;
+    }
+    
+    List getSampleExceptions() {
+        if (null == exceptions) {
+            ArrayList result = new ArrayList();
+            int i = 1;
+            result.add(new Error("Error " + i++));
+            result.add(new RuntimeException("RuntimeException " + i++));
+            result.add(new IOException("IOException " + i++));
+            exceptions = result;
+        }
+        return exceptions;
+    }
+
+    List getSampleLocations() {
+        if (null == locations) {
+            ArrayList result = new ArrayList();
+            File file = new File("testsrc/org/aspectj/testing/util/MessageUtilTest.java");
+            result.add(new SourceLocation(file, 1, 2, 1));
+            result.add(new SourceLocation(file, 100, 100, 0));
+            locations = result;
+        }
+        return locations;
+    }
+
+    MessageHandler getSampleMessages() {
+        MessageHandler result = new MessageHandler();
+        for (Iterator kinds = IMessage.KINDS.iterator(); kinds.hasNext();) {
+                       IMessage.Kind kind = (IMessage.Kind) kinds.next();
+                       for (Iterator locs = getSampleLocations().iterator(); locs.hasNext();) {
+                               ISourceLocation sourceLoc = (ISourceLocation) locs.next();
+                for (Iterator texts = getSampleMessageTexts().iterator();
+                    texts.hasNext();
+                                       ) {
+                                       String text = (String) texts.next();
+                                       for (Iterator exs = getSampleExceptions().iterator();
+                                               exs.hasNext();
+                                               ) {
+                                               Throwable thrown = (Throwable) exs.next();
+                                           result.handleMessage(new Message(text, kind, thrown, sourceLoc));
+                                       }
+                    result.handleMessage(new Message(text, kind, null, sourceLoc));
+                               }
+                result.handleMessage(new Message("", kind, null, sourceLoc));
+                       }
+            result.handleMessage(new Message("", kind, null, null));
+               }
+        return result;
+    }
+    
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/StreamGrabberTest.java b/testing/testsrc/org/aspectj/testing/util/StreamGrabberTest.java
new file mode 100644 (file)
index 0000000..9cd882f
--- /dev/null
@@ -0,0 +1,112 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+/*
+ * StreamGrabberTest.java created on May 16, 2002
+ *
+ */
+package org.aspectj.testing.util;
+
+
+import java.io.PrintStream;
+
+import junit.framework.TestCase;
+import junit.textui.TestRunner;
+
+/**
+ * 
+ * @author isberg
+ */
+public class StreamGrabberTest extends TestCase {
+
+
+    private static final String ME 
+        = "org.aspectj.testing.util.StreamGrabberTest";
+
+    /** @param args ignored */
+    public static void main(String[] args) {
+        TestRunner.main(new String[] {ME});
+    }
+
+    public StreamGrabberTest(String s) { super(s); }
+    
+    public void testHide() {
+        PrintStream restore = System.out;
+        System.setOut(new PrintStream(NullPrintStream.NULL_OutputStream));
+        System.out.println("OutputStream should not print!!!!!!!!!!!!!!!!!!!");
+        System.setOut(new PrintStream(NullPrintStream.NULL_PrintStream));
+        System.out.println("PrintStream should not print!!!!!!!!!!!!!!!!!!!");
+        System.setOut(restore); 
+    }
+    
+    /**
+     * Test StreamSniffer by setting up a delegate System.out
+     * and a normal System.out (delegating to System.out)
+     * and verifying that both get the same result.
+     */
+    public void testGrab() {
+        StringBuffer delegate = new StringBuffer();
+        StreamSniffer out = new StreamSniffer(System.out);
+        out.setBuffer(delegate);
+        System.setOut(new PrintStream(out));
+        StreamSniffer g = new StreamSniffer(System.out);
+        System.setOut(new PrintStream(g));
+        StringBuffer buf = new StringBuffer();
+        g.setBuffer(buf);
+
+        printLoop("f", buf, delegate);
+        printLoop("now is the time for all good men...", buf, delegate);
+        printlnLoop("f", buf, delegate);
+        printlnLoop("now is the time for all good men...", buf, delegate);           
+    }
+
+    private void printLoop(String expect, StringBuffer buf, StringBuffer delegate) {
+        System.out.print(expect);
+        String actual = buf.toString();
+        String delegateActual = delegate.toString();
+        assertTrue(expect + "=" + actual, expect.equals(actual));
+        assertTrue(expect + "=" + delegateActual, expect.equals(delegateActual));
+        buf.setLength(0);
+        delegate.setLength(0);
+        System.out.print(expect);
+        
+        actual = buf.toString();
+        delegateActual = delegate.toString();
+        assertTrue(expect + "=" + actual, expect.equals(actual));
+        assertTrue(expect + "=" + delegateActual, expect.equals(delegateActual));
+        buf.setLength(0);
+        delegate.setLength(0);
+    }
+    
+    private void printlnLoop(String expect, StringBuffer buf, StringBuffer delegate) {
+        // copy/paste of printLoop, using println
+        expect = expect.trim();
+        System.out.println(expect);
+        String actual = buf.toString().trim();
+        String delegateActual = delegate.toString().trim();
+        assertTrue(expect + "=" + actual, expect.equals(actual));
+        assertTrue(expect + "=" + delegateActual, expect.equals(delegateActual));
+        buf.setLength(0);
+        delegate.setLength(0);
+        
+        System.out.println(expect);
+        actual = buf.toString().trim();
+        delegateActual = delegate.toString().trim();
+        assertTrue(expect + "=" + actual, expect.equals(actual));
+        assertTrue(expect + "=" + delegateActual, expect.equals(delegateActual));
+        buf.setLength(0);
+        delegate.setLength(0);
+    }
+
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/TestDiffsTest.java b/testing/testsrc/org/aspectj/testing/util/TestDiffsTest.java
new file mode 100644 (file)
index 0000000..4ce7792
--- /dev/null
@@ -0,0 +1,114 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.ListIterator;
+
+import junit.framework.TestCase;
+import org.aspectj.util.FileUtil;
+
+/**
+ * 
+ */
+public class TestDiffsTest extends TestCase {
+
+   /**
+     * Expected results in test below.
+     */
+    private static void genTestInput(File expected, File actual) throws IOException {
+        FileWriter writer = null;
+        try {
+            writer = new FileWriter(expected);
+            PrintWriter pw = new PrintWriter(writer);
+            pw.println("PASS passed in both");
+            pw.println("## random text to ignore: " + System.currentTimeMillis());
+            pw.println("FAIL failed in both");
+            pw.println("PASS failed in actual (broken)");
+            pw.println("FAIL passed in actual (fixed)");
+            pw.println("PASS    not in actual (missing-pass)");
+            pw.println("FAIL    not in actual (missing-fail)");
+            pw.flush();
+            writer.close();
+            
+            writer = new FileWriter(actual);
+            pw = new PrintWriter(writer);
+            pw.println("PASS passed in actual (fixed)");
+            pw.println("## random text to ignore: " + System.currentTimeMillis());
+            pw.println("PASS  not in expected (added-pass)");
+            pw.println("FAIL failed in both");
+            pw.println("PASS passed in both");
+            pw.println("FAIL failed in actual (broken)");
+            pw.println("FAIL  not in expected (added-fail)");
+            pw.flush();
+            writer.close();
+            writer = null;
+        } finally {
+            if (null != writer) {
+                try { writer.close(); }
+                catch (IOException e) { } // ignore                    
+            }
+        }
+    }
+
+    ArrayList tempFiles;
+       /**
+        * Constructor for FileUtilTest.
+        * @param arg0
+        */
+       public TestDiffsTest(String arg0) {
+               super(arg0);
+          tempFiles = new ArrayList();
+    }
+
+    public void tearDown() {
+        for (ListIterator iter = tempFiles.listIterator(); iter.hasNext();) {
+            File dir = (File) iter.next();
+            FileUtil.deleteContents(dir);
+            dir.delete();
+            iter.remove();
+        }
+    }
+
+
+    public void testCompareResults() {
+        File tempDir = org.aspectj.util.FileUtil.getTempDir("testCompareResults");
+        File expected = new File(tempDir, "expected.txt");
+        File actual = new File(tempDir, "actual.txt");
+        tempFiles.add(expected);
+        tempFiles.add(actual);
+        try {
+            genTestInput(expected, actual);
+        } catch (IOException e) {
+            assertTrue(e.getMessage(), false);
+        }
+        TestDiffs result = TestDiffs.compareResults(expected, actual);
+        assertEquals(2, result.missing.size());
+        assertEquals(2, result.added.size());
+        assertEquals(1, result.fixed.size());
+        assertEquals(1, result.broken.size());
+        assertEquals(3, result.actualFailed.size());
+        assertEquals(3, result.actualPassed.size());
+        assertEquals(6, result.actual.size());
+        assertEquals(3, result.expectedFailed.size());
+        assertEquals(3, result.expectedPassed.size());
+        assertEquals(6, result.expected.size());
+        assertEquals(1, result.stillFailing.size());
+        assertEquals(1, result.stillPassing.size());
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/UtilTests.java b/testing/testsrc/org/aspectj/testing/util/UtilTests.java
new file mode 100644 (file)
index 0000000..3ec14d7
--- /dev/null
@@ -0,0 +1,36 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util;
+
+import junit.framework.*;
+
+public class UtilTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(UtilTests.class.getName());
+        //$JUnit-BEGIN$
+        suite.addTestSuite(BridgeUtilTest.class); 
+        suite.addTestSuite(FileUtilTest.class); 
+        suite.addTestSuite(IteratorWrapperTest.class); 
+        suite.addTestSuite(LangUtilTest.class); 
+        suite.addTestSuite(MessageUtilTest.class); 
+        suite.addTestSuite(StreamGrabberTest.class); 
+        //$JUnit-END$
+        return suite;
+    }
+
+    public UtilTests(String name) { super(name); }
+
+}  
diff --git a/testing/testsrc/org/aspectj/testing/xml/AjcSpecXmlReaderTest.java b/testing/testsrc/org/aspectj/testing/xml/AjcSpecXmlReaderTest.java
new file mode 100644 (file)
index 0000000..157cf2d
--- /dev/null
@@ -0,0 +1,236 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.xml;
+
+import org.aspectj.bridge.MessageHandler;
+import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.harness.bridge.AjcSpecTest;
+import org.aspectj.testing.harness.bridge.CompilerRun;
+import org.aspectj.testing.harness.bridge.FlatSuiteReader;
+import org.aspectj.testing.harness.bridge.AjcTest;
+import org.aspectj.testing.run.IRunStatus;
+import org.aspectj.testing.run.IRunValidator;
+import org.aspectj.testing.run.RunValidator;
+import org.aspectj.util.LangUtil;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class AjcSpecXmlReaderTest extends TestCase {
+
+       ArrayList tempFiles = new ArrayList();
+       /**
+        * Constructor for AjcSpecXmlReaderTest.
+        * @param name
+        */
+       public AjcSpecXmlReaderTest(String name) {
+               super(name);
+       }
+
+    public void setUp() {
+       tempFiles.clear();
+        //System.out.println("XXX test requires compiler and bridgeImpl projects on classpath");
+    }
+
+    public void tearDown() {
+       if (!LangUtil.isEmpty(tempFiles)) {
+               for (Iterator iter = tempFiles.iterator(); iter.hasNext();) {
+                               File file = (File) iter.next();
+                               if (file.canRead()) {
+                                       file.delete();
+                               }
+                       }
+               }
+    }
+    
+    /** test that all AjcSpecXmlReader.me.expectedProperties() are bean-writable */
+    public void testBeanInfo() throws IntrospectionException {
+        AjcSpecXmlReader me = AjcSpecXmlReader.getReader();
+        AjcSpecXmlReader.BProps[] expected = me.expectedProperties();
+        PropertyDescriptor[] des;
+        for (int i = 0; i < expected.length; i++) {
+            Class clazz = expected[i].cl;
+            BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
+            assertTrue(null != beanInfo);
+            des = beanInfo.getPropertyDescriptors();
+            for (int j = 0; j < expected[i].props.length; j++) {
+                               String name = expected[i].props[j];
+                String fqn = clazz.getName() + "." + name;
+                boolean gotIt = false;
+                for (int k = 0; k < des.length; k++) {
+                    String desName = des[k].getName();
+                    if (name.equals(desName)) {
+                        assertTrue(fqn, null != des[k].getWriteMethod());
+                        gotIt = true;
+                    }         
+                }
+                assertTrue("no such property: " + fqn, gotIt);
+                       }
+               }
+        
+    }
+    public void testAjcTests() throws IOException { 
+        checkXmlRoundTrip("../tests/ajcTests");
+    }
+
+    public void testAjcTests10() throws IOException { 
+        checkXmlRoundTrip("../tests/ajcTests10");
+    }
+
+    public void testAjcTestsBroken() throws IOException { 
+        checkXmlRoundTrip("../tests/ajcTestsBroken");
+    }
+
+    public void testAjcTestsAttic() throws IOException { 
+        checkXmlRoundTrip("../tests/ajcTestsAttic");
+    }
+    
+    public void testAjcHarnessTests() throws IOException { 
+        checkXmlRoundTrip("../tests/ajcHarnessTests");
+    }
+
+    void checkXmlRoundTrip(String path) throws IOException {
+        String xmlPath = path + ".xml";
+        String xml2Path = path + ".tmp.xml";
+
+        final File file1 = new File(xmlPath);
+        final ArrayList toDelete = new ArrayList();
+        final AjcSpecXmlReader writer = AjcSpecXmlReader.getReader();
+
+        assertTrue("" + file1, file1.canRead());
+        AjcTest.Suite.Spec suite1 = writer.readAjcSuite(file1);
+        assertNotNull(suite1);
+
+        File file2 = new File(xml2Path);
+        String warning = writer.writeSuiteToXmlFile(file2, suite1);
+        toDelete.add(file2);
+        assertTrue(warning, null == warning);
+
+        AjcTest.Suite.Spec suite2 = writer.readAjcSuite(file1);
+        assertNotNull(suite2);
+        AjcSpecTest.sameAjcSuiteSpec(suite1, suite2, this);
+        
+        for (Iterator iter = toDelete.iterator(); iter.hasNext();) {
+          ((File) iter.next()).delete();          
+      }
+    }
+
+    void checkRoundTrip(String path) throws IOException, Exception {
+        // XXX once .txt gone, add bugId and keywords to test
+        String txtPath = path + ".txt";
+        String xmlPath = path + ".tmp.xml";
+        String xml2Path = path + ".tmp2.xml";
+
+        // read flat, write the xml variant, read back, and compare
+        AjcSpecXmlReader writer = AjcSpecXmlReader.getReader();
+        File file0 = new File(txtPath);
+        File file1 = new File(xmlPath);
+        ArrayList toDelete = new ArrayList();
+        AjcTest.Suite.Spec suite0 = null;
+        if (file0.canRead()) {
+            System.out.println("reading " + file0);
+            suite0 = FlatSuiteReader.ME.readSuite(file0);            
+            String warning = writer.writeSuiteToXmlFile(file1, suite0);
+            toDelete.add(file1);
+            assertTrue(warning, null == warning);
+        } else {
+            file1 = new File(path + ".xml");
+            if (file1.canRead()) {
+                System.out.println("reading " + file1);
+                suite0 = writer.readAjcSuite(file1);
+            } else {
+               System.err.println("Skipping as not in module: " + file0);
+               return;
+            }
+        }
+        assertNotNull(suite0);
+        
+        //System.err.println("----------------------- suite0 " + txtPath);
+        //suite0.printAll(System.err, "");
+        assertTrue("" + file1, file1.canRead());
+        System.out.println("reading " + file1);
+        AjcTest.Suite.Spec suite1 = writer.readAjcSuite(file1);
+        assertNotNull(suite1);
+        //System.err.println("----------------------- suite1 " + xmlPath);
+        //suite0.printAll(System.err, "");
+        AjcSpecTest.sameAjcSuiteSpec(suite0, suite1, this);
+
+        // same for second-generation xml
+        file1 = new File(xml2Path);
+        String warning = writer.writeSuiteToXmlFile(file1, suite1);
+        toDelete.add(file1);
+        // XXX enable later assertTrue(warning, null == warning);
+        if (null != warning) {
+            System.out.println("warning " + file1 + ": " + warning);
+        }
+        System.out.println("reading " + file1);
+        AjcTest.Suite.Spec suite2 = writer.readAjcSuite(file1);
+        assertNotNull(suite2);
+        //System.err.println("----------------------- suite2 " + xml2Path);
+        AjcSpecTest.sameAjcSuiteSpec(suite1, suite2, this);
+        AjcSpecTest.sameAjcSuiteSpec(suite0, suite2, this);
+        
+        for (Iterator iter = toDelete.iterator(); iter.hasNext();) {
+                       ((File) iter.next()).delete();                  
+               }
+    }
+}
+
+       // ------------------- XXX retry execution round-trips when eclipse working
+        //AjcSpecTest.sameAjcTestSpec(txtList, xmlSpec, this);
+        
+//        List xmlList = writer.readAjcTests(xmlFile);
+//        AjcSpecTest.sameAjcTestLists(txtList, xmlList, this);
+//        List xml2List = writer.readAjcTests(xmlFile);
+//        AjcSpecTest.sameAjcTestLists(xmlList, xml2List, this);
+
+
+        // ------------------ now run them both and compare results
+//        // run the flat and xml variants, then compare
+//        MessageHandler xmlHandler = new MessageHandler();
+//        IRunStatus xmlStatus = runFile(xmlPath, xmlHandler);
+//        MessageHandler txtHandler = new MessageHandler();
+//        IRunStatus txtStatus = runFile(txtPath, txtHandler);
+//        
+//        
+//        // both should pass or fail..
+//        IRunValidator v = RunValidator.NORMAL;
+//        boolean xmlPassed = v.runPassed(xmlStatus);
+//        boolean txtPassed = v.runPassed(txtStatus);
+//        boolean passed = (xmlPassed == txtPassed);
+//        if (!xmlPassed) {
+//            MessageUtil.print(System.err, xmlStatus);
+//        }
+//        if (!txtPassed) {
+//            MessageUtil.print(System.err, txtStatus);
+//        }
+//        if (!passed) { // calculate diffs
+//        }
+//    IRunStatus runFile(String path, MessageHandler handler) throws IOException {
+//        Main runner = new Main(new String[] {path}, handler);
+//        String id = "AjcSpecXmlReaderTest.runFile(" + path + ")";
+//        return runner.runMain(id, handler, System.err); 
+//    }
diff --git a/testing/testsrc/org/aspectj/testing/xml/TestingXmlTests.java b/testing/testsrc/org/aspectj/testing/xml/TestingXmlTests.java
new file mode 100644 (file)
index 0000000..fbefdba
--- /dev/null
@@ -0,0 +1,31 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.xml;
+
+import junit.framework.*;
+
+public class TestingXmlTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(TestingXmlTests.class.getName());
+        //$JUnit-BEGIN$
+        suite.addTestSuite(AjcSpecXmlReaderTest.class); 
+        //$JUnit-END$
+        return suite;
+    }
+
+    public TestingXmlTests(String name) { super(name); }
+
+}  
diff --git a/testing/testsrc/org/aspectj/testing/xml/XMLWriterTest.java b/testing/testsrc/org/aspectj/testing/xml/XMLWriterTest.java
new file mode 100644 (file)
index 0000000..8079077
--- /dev/null
@@ -0,0 +1,44 @@
+/* *******************************************************************
+ * Copyright (c) 1999-2001 Xerox Corporation, 
+ *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Xerox/PARC     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.xml;
+
+import java.util.Arrays;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class XMLWriterTest extends TestCase {
+
+       public XMLWriterTest(String name) {
+               super(name);
+       }
+
+    /** @see LangUtilTest#testCommaSplit() */
+    public void testUnflattenList() { 
+        checkUnflattenList("", new String[] {""});
+        checkUnflattenList("1", new String[] {"1"});
+        checkUnflattenList(" 1 2 ", new String[] {"1 2"});
+        checkUnflattenList(" 1 , 2 ", new String[] {"1", "2"});
+        checkUnflattenList("1,2,3,4", new String[] {"1", "2", "3", "4"});
+    }
+    
+    void checkUnflattenList(String input, String[] expected) {
+        String[] actual = XMLWriter.unflattenList(input);
+        String a = "" + Arrays.asList(actual);
+        String e = "" + Arrays.asList(expected);
+        assertTrue(e + "==" + a, e.equals(a));
+    }   
+}