Ajctest.java

/* *******************************************************************
 * 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 Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Xerox/PARC     initial implementation
 * ******************************************************************/


package org.aspectj.internal.tools.ant.taskdefs;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
//import java.util.Collection;
import java.util.*;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.border.BevelBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.text.StyleContext;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Delete;
//import org.apache.tools.ant.taskdefs.ExecTask;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.taskdefs.Mkdir;
import org.apache.tools.ant.taskdefs.StreamPumper;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.aspectj.util.LangUtil;

@SuppressWarnings("deprecation")
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 Stats ajdocStats = new Stats();
    private Stats ajcStats   = new Stats();
    private Stats runStats   = new Stats();
//    private 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<Argument> args = new Vector<>();
    private List<Testset> testsets = new Vector<>();
    private Path classpath;
    private Path internalclasspath;
    private File destdir;
    private File dir;
    private File errorfile;
    private List<Run> testclasses = new Vector<>();
    private boolean nocompile;
    private Ajdoc ajdoc = null;
    private boolean noclean;
    private boolean noverify;
    private List<String> 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 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<String> 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<Argfile> argfileNames = new Vector<>();
        public List<File> argfiles;
        public List<File> files;
        public List<Argument> args = new Vector<>();
        public String classname;
        private boolean havecludes = false;
        private List<Run> testclasses = new Vector<>();
        private Path classpath;
        private Path internalclasspath;
        private Ajdoc ajdoc = null;
        private boolean fork = false;
        private boolean noclean;
        private List<String> depends = new Vector<>();
        public String toString() {
            String str = "";
            if (files.size() > 0) {
                str += "files:" + "\n";
				for (File file : files) {
					str += "\t" + file + "\n";
				}
            }
            if (argfiles.size() > 0) {
                str += "argfiles:" + "\n";
				for (File argfile : argfiles) {
					str += "\t" + argfile + "\n";
				}
            }
            if (args.size() > 0) {
                str += "args:" + "\n";
				for (Argument arg : args) {
					str += "\t" + arg + "\n";
				}
            }
            if (testclasses.size() > 0) {
                str += "classes:" + "\n";
				for (Run testclass : testclasses) {
					str += "\t" + testclass + "\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 (Argfile argfileName : argfileNames) {
				String name = argfileName.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 (String name : filenames) {
					if (name.endsWith(".java")) {
						File file = new File(src, name);
						if (check(file, name, location)) files.add(file);
					}
				}
            }
			for (Run run : Ajctest.this.testclasses) {
				this.testclasses.add(run);
			}
            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 (Run testclass : this.testclasses) {
					testclass.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(" ", 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 (Object o : list) {
			log(space + "  " + o);
		}
    }

    private void execute(Testset testset, List<Arg> args) throws BuildException {
        if (testset.files.size() > 0) {
            log("\tfiles:");
			for (File file : testset.files) {
				log("\t  " + file);
			}
        }
        if (testset.argfiles.size() > 0) {
            log("\targfiles:");
			for (File file : testset.argfiles) {
				log("\t  " + file);
			}
        }
        if (args.size() > 0) {
            log("\targs:");
			for (Arg arg : args) {
				log("\t  " + arg);
			}
        }
        if (testset.testclasses.size() > 0) {
            log("\tclasses:");
			for (Run testclass : testset.testclasses) {
				log("\t  " + testclass);
			}
        }
        if (!testset.noclean &&
            (!isSet("noclean") && !isSet("nocompile"))) {
            delete(destdir);
            make(destdir);
        }
        delete(workingdir);
        make(workingdir);
		for (String depend : testset.depends) {
			String target = depend + "";
			// 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 (Run testclass : testset.testclasses) {
				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<Testset,List<List<Arg>>> testsetToArgcombo = new HashMap<>();
        List<Integer> argcombos = new Vector<>();
        for (Testset testset: testsets) {
            testset.resolve();
            List<Argument> bothargs = new Vector<>(args);
            bothargs.addAll(testset.args);
            List<List<Arg>> argcombo = argcombo(bothargs);
            argcombos.add(argcombo.size());
            testsetToArgcombo.put(testset, argcombo);
        }
        while (!testsetToArgcombo.isEmpty()) {
            int testsetCounter = 1;
            for (Iterator<Testset> iter = testsets.iterator(); iter.hasNext(); testsetCounter++) {
                Testset testset = iter.next();
                List<List<Arg>> argcombo = testsetToArgcombo.get(testset);
                if (argcombo.size() == 0) {
                    testsetToArgcombo.remove(testset);
                    continue;
                }
                List<Arg> args = argcombo.remove(0);
                final String startStr = "Testset " + testsetCounter + " of " + testsets.size();
                String str = startStr + " / Combo " + testsetCounter + " 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<Failure> 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 {
                        StringBuilder sb = new StringBuilder("\n");
                        int errs = Ajctest.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", current.ajdocStats.goods+"",   current.ajdocStats.fails+"",
                            "ajc",   current.ajcStats.goods  +"",   current.ajcStats.fails  +"",
                            "run",   current.runStats.goods  +"",   current.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() +
                                        current.ajdocStats.fails +
                                        current.ajcStats.fails   +
                                        current.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<List<Arg>> argcombo(List<Argument> arguments) {
        List<Argument> combos = new Vector<>();
        List<Arg> always = new Vector<>();
		for (Argument arg : arguments) {
			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<List<Arg>> argcombo = combinations(combos);
		for (Arg arg : always) {
			for (List<Arg> argList : argcombo) {
				argList.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();
			Collections.addAll(this.args, cmds);
        }
        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 (Object item : args) {
				Arg arg = (Arg) item;
				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 (Object o : args) {
				Arg arg = (Arg) o;
				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 (File value : testset.files) {
				cmd.createArgument().setFile(value);
			}
			for (File file : testset.argfiles) {
				cmd.createArgument().setValue("-argfile");
				cmd.createArgument().setFile(file);
			}
            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<List<Arg>> combinations(List<Argument> arglist) {
        List<List<Arg>> result = new Vector<>();
        result.add(new Vector<>());
		for (Argument arg : arglist) {
			int N = result.size();
			for (int i = 0; i < N; i++) {
				List<Arg> to = result.remove(0);
				for (String s : arg.values) {
					List<Arg> newlist = new Vector<>(to);
					Object val = s;
					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 (Object name : names) {
					model.addColumn(name);
				}
                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,
						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 (Object o : os) {
						String name = o + "";
						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;
                }
            }
        }
    }

}