From f4d5e9485ae910220e06b0b6c778587ed6b4ebb3 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Wed, 30 Jan 2019 16:15:14 -0800 Subject: mavenizing testing - done --- .../internal/tools/ant/taskdefs/Ajctest.java | 1900 ++++++++++++++++++++ .../internal/tools/ant/taskdefs/MainWrapper.java | 181 ++ .../src/test/java/org/aspectj/testing/AjcTest.java | 169 ++ .../src/test/java/org/aspectj/testing/AntSpec.java | 265 +++ .../testing/AutowiredXMLBasedAjcTestCase.java | 154 ++ .../test/java/org/aspectj/testing/CompileSpec.java | 336 ++++ .../org/aspectj/testing/ExpectedMessageSpec.java | 94 + .../test/java/org/aspectj/testing/FileSpec.java | 93 + .../test/java/org/aspectj/testing/ITestStep.java | 28 + .../java/org/aspectj/testing/MakeTestClass.java | 118 ++ .../test/java/org/aspectj/testing/OutputLine.java | 42 + .../test/java/org/aspectj/testing/OutputSpec.java | 134 ++ .../src/test/java/org/aspectj/testing/RunSpec.java | 262 +++ .../org/aspectj/testing/TestingModuleTests.java | 36 - .../test/java/org/aspectj/testing/WeaveSpec.java | 164 ++ .../org/aspectj/testing/XMLBasedAjcTestCase.java | 522 ++++++ .../XMLBasedAjcTestCaseForJava10OrLater.java | 31 + .../XMLBasedAjcTestCaseForJava11OrLater.java | 31 + .../XMLBasedAjcTestCaseForJava9OrLater.java | 32 + .../org/aspectj/testing/ajde/CompileCommand.java | 439 +++++ .../testing/harness/bridge/AbstractRunSpec.java | 918 ++++++++++ .../testing/harness/bridge/AjcMessageHandler.java | 343 ++++ .../aspectj/testing/harness/bridge/AjcTest.java | 586 ++++++ .../testing/harness/bridge/CompilerRun.java | 1840 +++++++++++++++++++ .../aspectj/testing/harness/bridge/DirChanges.java | 614 +++++++ .../testing/harness/bridge/FlatSuiteReader.java | 376 ++++ .../aspectj/testing/harness/bridge/Globals.java | 162 ++ .../aspectj/testing/harness/bridge/IAjcRun.java | 44 + .../aspectj/testing/harness/bridge/IRunSpec.java | 24 + .../testing/harness/bridge/IncCompilerRun.java | 503 ++++++ .../aspectj/testing/harness/bridge/JavaRun.java | 899 +++++++++ .../testing/harness/bridge/RunSpecIterator.java | 256 +++ .../aspectj/testing/harness/bridge/Sandbox.java | 575 ++++++ .../testing/harness/bridge/TestingBridgeTests.java | 38 - .../aspectj/testing/harness/bridge/Validator.java | 564 ++++++ .../test/java/org/aspectj/testing/run/IRun.java | 62 + .../java/org/aspectj/testing/run/IRunIterator.java | 62 + .../java/org/aspectj/testing/run/IRunListener.java | 38 + .../java/org/aspectj/testing/run/IRunStatus.java | 195 ++ .../org/aspectj/testing/run/IRunValidator.java | 28 + .../java/org/aspectj/testing/run/RunIterator.java | 136 ++ .../java/org/aspectj/testing/run/RunListener.java | 116 ++ .../java/org/aspectj/testing/run/RunListeners.java | 89 + .../java/org/aspectj/testing/run/RunStatus.java | 442 +++++ .../java/org/aspectj/testing/run/RunValidator.java | 210 +++ .../test/java/org/aspectj/testing/run/Runner.java | 510 ++++++ .../aspectj/testing/run/WrappedRunIterator.java | 72 + .../testing/taskdefs/AjcTaskCompileCommand.java | 294 +++ .../org/aspectj/testing/taskdefs/TaskdefTests.java | 29 - .../testing/util/AccumulatingFileFilter.java | 53 + .../java/org/aspectj/testing/util/BridgeUtil.java | 517 ++++++ .../aspectj/testing/util/CollectorFileFilter.java | 88 + .../test/java/org/aspectj/testing/util/Diffs.java | 547 ++++++ .../java/org/aspectj/testing/util/FileUtil.java | 745 ++++++++ .../java/org/aspectj/testing/util/IntRange.java | 118 ++ .../org/aspectj/testing/util/IntValidator.java | 21 + .../org/aspectj/testing/util/IteratorWrapper.java | 147 ++ .../java/org/aspectj/testing/util/LangUtil.java | 1271 +++++++++++++ .../java/org/aspectj/testing/util/LineReader.java | 204 +++ .../java/org/aspectj/testing/util/LinkCheck.java | 546 ++++++ .../test/java/org/aspectj/testing/util/Node.java | 162 ++ .../org/aspectj/testing/util/NullPrintStream.java | 95 + .../org/aspectj/testing/util/ObjectChecker.java | 51 + .../org/aspectj/testing/util/ProxyPrintStream.java | 104 ++ .../java/org/aspectj/testing/util/RunUtils.java | 368 ++++ .../java/org/aspectj/testing/util/SFileReader.java | 184 ++ .../testing/util/StandardObjectChecker.java | 127 ++ .../org/aspectj/testing/util/StreamSniffer.java | 76 + .../org/aspectj/testing/util/StreamsHandler.java | 217 +++ .../aspectj/testing/util/StringAccumulator.java | 99 + .../org/aspectj/testing/util/StringVisitor.java | 28 + .../aspectj/testing/util/StructureModelUtil.java | 263 +++ .../org/aspectj/testing/util/TestClassLoader.java | 157 ++ .../java/org/aspectj/testing/util/TestDiffs.java | 360 ++++ .../org/aspectj/testing/util/UtilLineReader.java | 212 +++ .../java/org/aspectj/testing/util/UtilTests.java | 39 - .../org/aspectj/testing/util/ValidFileFilter.java | 123 ++ .../java/org/aspectj/testing/util/WebInstall.java | 208 +++ .../org/aspectj/testing/util/options/Option.java | 707 ++++++++ .../org/aspectj/testing/util/options/Options.java | 152 ++ .../aspectj/testing/util/options/OptionsTests.java | 30 - .../org/aspectj/testing/util/options/Values.java | 714 ++++++++ .../org/aspectj/testing/xml/AjcSpecXmlReader.java | 526 ++++++ .../java/org/aspectj/testing/xml/IXmlWritable.java | 25 + .../aspectj/testing/xml/MessageListXmlReader.java | 215 +++ .../java/org/aspectj/testing/xml/SoftMessage.java | 368 ++++ .../aspectj/testing/xml/SoftSourceLocation.java | 133 ++ .../org/aspectj/testing/xml/TestingXmlTests.java | 35 - .../java/org/aspectj/testing/xml/XMLWriter.java | 365 ++++ 89 files changed, 25249 insertions(+), 207 deletions(-) create mode 100644 testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java create mode 100644 testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java create mode 100644 testing/src/test/java/org/aspectj/testing/AjcTest.java create mode 100644 testing/src/test/java/org/aspectj/testing/AntSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/AutowiredXMLBasedAjcTestCase.java create mode 100644 testing/src/test/java/org/aspectj/testing/CompileSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/ExpectedMessageSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/FileSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/ITestStep.java create mode 100644 testing/src/test/java/org/aspectj/testing/MakeTestClass.java create mode 100644 testing/src/test/java/org/aspectj/testing/OutputLine.java create mode 100644 testing/src/test/java/org/aspectj/testing/OutputSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/RunSpec.java delete mode 100644 testing/src/test/java/org/aspectj/testing/TestingModuleTests.java create mode 100644 testing/src/test/java/org/aspectj/testing/WeaveSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCase.java create mode 100644 testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava10OrLater.java create mode 100644 testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava11OrLater.java create mode 100644 testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava9OrLater.java create mode 100644 testing/src/test/java/org/aspectj/testing/ajde/CompileCommand.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/AbstractRunSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/AjcMessageHandler.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/AjcTest.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/CompilerRun.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/DirChanges.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/FlatSuiteReader.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/Globals.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/IAjcRun.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/IRunSpec.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/IncCompilerRun.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/JavaRun.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/RunSpecIterator.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/Sandbox.java delete mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/TestingBridgeTests.java create mode 100644 testing/src/test/java/org/aspectj/testing/harness/bridge/Validator.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/IRun.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/IRunIterator.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/IRunListener.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/IRunStatus.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/IRunValidator.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/RunIterator.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/RunListener.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/RunListeners.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/RunStatus.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/RunValidator.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/Runner.java create mode 100644 testing/src/test/java/org/aspectj/testing/run/WrappedRunIterator.java create mode 100644 testing/src/test/java/org/aspectj/testing/taskdefs/AjcTaskCompileCommand.java delete mode 100644 testing/src/test/java/org/aspectj/testing/taskdefs/TaskdefTests.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/AccumulatingFileFilter.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/BridgeUtil.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/CollectorFileFilter.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/Diffs.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/FileUtil.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/IntRange.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/IntValidator.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/IteratorWrapper.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/LangUtil.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/LineReader.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/LinkCheck.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/Node.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/NullPrintStream.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/ObjectChecker.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/ProxyPrintStream.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/RunUtils.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/SFileReader.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/StandardObjectChecker.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/StreamSniffer.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/StreamsHandler.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/StringAccumulator.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/StringVisitor.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/StructureModelUtil.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/TestClassLoader.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/TestDiffs.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/UtilLineReader.java delete mode 100644 testing/src/test/java/org/aspectj/testing/util/UtilTests.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/ValidFileFilter.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/WebInstall.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/options/Option.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/options/Options.java delete mode 100644 testing/src/test/java/org/aspectj/testing/util/options/OptionsTests.java create mode 100644 testing/src/test/java/org/aspectj/testing/util/options/Values.java create mode 100644 testing/src/test/java/org/aspectj/testing/xml/AjcSpecXmlReader.java create mode 100644 testing/src/test/java/org/aspectj/testing/xml/IXmlWritable.java create mode 100644 testing/src/test/java/org/aspectj/testing/xml/MessageListXmlReader.java create mode 100644 testing/src/test/java/org/aspectj/testing/xml/SoftMessage.java create mode 100644 testing/src/test/java/org/aspectj/testing/xml/SoftSourceLocation.java delete mode 100644 testing/src/test/java/org/aspectj/testing/xml/TestingXmlTests.java create mode 100644 testing/src/test/java/org/aspectj/testing/xml/XMLWriter.java (limited to 'testing/src/test/java/org') diff --git a/testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java b/testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java new file mode 100644 index 000000000..8fb274176 --- /dev/null +++ b/testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/Ajctest.java @@ -0,0 +1,1900 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * 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.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.Vector; + +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; + +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 args = new Vector<>(); + private List testsets = new Vector<>(); + private Path classpath; + private Path internalclasspath; + private File destdir; + private File dir; + 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 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 = 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();) { + 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 = 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 (Testset testset: testsets) { + 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 testsetCounter = 1; + for (Iterator iter = testsets.iterator(); iter.hasNext(); testsetCounter++) { + Testset testset = iter.next(); + List> argcombo = testsetToArgcombo.get(testset); + if (argcombo.size() == 0) { + testsetToArgcombo.remove(testset); + continue; + } + List 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; + *

+ * (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. + *

todo: relies on name/value of property "taskdef.jar" + * to add jar with wrapper to invoking classpath. + *

+ * 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 + * setClassesDir(String) + *

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: + *

  • fork to get classpath and VM properties right
  • + *
  • set VM property
  • + *
  • add ${ajctest.wrapper.jar} (with wrapper class) to the classpath
  • + */ + 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 = 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> argcombo(List arguments) { + List combos = new Vector<>(); + List always = new Vector<>(); + for (Iterator iter = arguments.iterator(); iter.hasNext();) { + Argument arg = 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 = iter.next(); + for (Iterator> comboiter = argcombo.iterator(); comboiter.hasNext();) { + 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 = iter.next(); + int N = result.size(); + for (int i = 0; i < N; i++) { + List to = 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/test/java/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java b/testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java new file mode 100644 index 000000000..6ce22df24 --- /dev/null +++ b/testing/src/test/java/org/aspectj/internal/tools/ant/taskdefs/MainWrapper.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + + + +package org.aspectj.internal.tools.ant.taskdefs; + +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.aspectj.testing.util.LangUtil; + +/** + * 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 PROP_NAME, but + * is set by adding the following to the command line: + * -DMainWrapper.classname="fully.qualified.Classname". + * This returns -1 if unable to load the main method, + * 1 if the invoked method throws an exception, and 0 otherwise. + */ +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/test/java/org/aspectj/testing/AjcTest.java b/testing/src/test/java/org/aspectj/testing/AjcTest.java new file mode 100644 index 000000000..4cb9a8722 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/AjcTest.java @@ -0,0 +1,169 @@ +/* ******************************************************************* + * Copyright (c) 2004,2018 IBM Corporation, contributors + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * ******************************************************************/ +package org.aspectj.testing; + +import java.util.ArrayList; +import java.util.List; + +import org.aspectj.tools.ajc.AjcTestCase; +import org.aspectj.util.LangUtil; + +/** + * @author Adrian Colyer + * @author Andy Clement + */ +public class AjcTest { + +// private static boolean is13VMOrGreater = true; + private static boolean is14VMOrGreater = true; + private static boolean is15VMOrGreater = false; + private static boolean is16VMOrGreater = false; + private static boolean is17VMOrGreater = false; + private static boolean is18VMOrGreater = false; + private static boolean is19VMOrGreater = false; + private static boolean is10VMOrGreater = false; + private static boolean is11VMOrGreater = false; + + static { // matching logic is also in org.aspectj.util.LangUtil + is14VMOrGreater = LangUtil.is14VMOrGreater(); + is15VMOrGreater = LangUtil.is15VMOrGreater(); + is16VMOrGreater = LangUtil.is16VMOrGreater(); + is17VMOrGreater = LangUtil.is17VMOrGreater(); + is18VMOrGreater = LangUtil.is18VMOrGreater(); + is19VMOrGreater = LangUtil.is19VMOrGreater(); + is10VMOrGreater = LangUtil.is10VMOrGreater(); + is11VMOrGreater = LangUtil.is11VMOrGreater(); + } + + private List testSteps = new ArrayList(); + + private String dir; + private String pr; + private String title; + private String keywords; + private String comment; + private String vmLevel = "1.3"; + + public AjcTest() { + } + + public void addTestStep(ITestStep step) { + testSteps.add(step); + step.setTest(this); + } + + public boolean runTest(AjcTestCase testCase) { + if (!canRunOnThisVM()) return false; + try { + System.out.print("TEST: " + getTitle() + "\t"); + for (ITestStep step: testSteps) { + step.setBaseDir(getDir()); + System.out.print("."); + step.execute(testCase); + } + } finally { + System.out.println("DONE"); + } + return true; + } + + public boolean canRunOnThisVM() { + if (vmLevel.equals("1.3")) return true; + boolean canRun = true; + if (vmLevel.equals("1.4")) canRun = is14VMOrGreater; + if (vmLevel.equals("1.5")) canRun = is15VMOrGreater; + if (vmLevel.equals("1.6")) canRun = is16VMOrGreater; + if (vmLevel.equals("1.7")) canRun = is17VMOrGreater; + if (vmLevel.equals("1.8")) canRun = is18VMOrGreater; + if (vmLevel.equals("1.9")) canRun = is19VMOrGreater; + if (vmLevel.equals("10")) canRun = is10VMOrGreater; + if (vmLevel.equals("11")) canRun = is11VMOrGreater; + if (!canRun) { + System.out.println("***SKIPPING TEST***" + getTitle()+ " needs " + getVmLevel() + + ", currently running on " + System.getProperty("java.vm.version")); + } + return canRun; + } + + /** + * @return Returns the comment. + */ + public String getComment() { + return comment; + } + /** + * @param comment The comment to set. + */ + public void setComment(String comment) { + this.comment = comment; + } + /** + * @return Returns the dir. + */ + public String getDir() { + return dir; + } + /** + * @param dir The dir to set. + */ + public void setDir(String dir) { + dir = "../tests/" + dir; + this.dir = dir; + } + /** + * @return Returns the keywords. + */ + public String getKeywords() { + return keywords; + } + /** + * @param keywords The keywords to set. + */ + public void setKeywords(String keywords) { + this.keywords = keywords; + } + /** + * @return Returns the pr. + */ + public String getPr() { + return pr; + } + /** + * @param pr The pr to set. + */ + public void setPr(String pr) { + this.pr = pr; + } + /** + * @return Returns the title. + */ + public String getTitle() { + return title; + } + /** + * @param title The title to set. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * @param vmLevel The vmLevel to set. + */ + public void setVm(String vmLevel) { + this.vmLevel = vmLevel; + } + + /** + * @return Returns the vmLevel. + */ + public String getVmLevel() { + return vmLevel; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/AntSpec.java b/testing/src/test/java/org/aspectj/testing/AntSpec.java new file mode 100644 index 000000000..ceba08b29 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/AntSpec.java @@ -0,0 +1,265 @@ +/******************************************************************************* + * Copyright (c) 2005 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://eclipse.org/legal/epl-v10.html + * + * Contributors: + * Alexandre Vasseur initial implementation + *******************************************************************************/ +package org.aspectj.testing; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.StringTokenizer; + +import org.apache.tools.ant.BuildEvent; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DefaultLogger; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.ProjectHelper; +import org.apache.tools.ant.Target; +import org.apache.tools.ant.taskdefs.Java; +import org.apache.tools.ant.types.Path; +import org.aspectj.tools.ajc.AjcTestCase; + +/** + * Element that allow to run an abritrary Ant target in a sandbox. + *

    + * Such a spec is used in a " XML element. The "target" is + * optional. If not set, default myAnt.xml target is used. The "file" file is looked up from the attribute. If + * "verbose" is set to "true", the ant -v output is piped, else nothing is reported except errors. + *

    + * The called Ant target benefits from 2 implicit variables: "${aj.sandbox}" points to the test current sandbox folder. "aj.path" is + * an Ant refid on the classpath formed with the sandbox folder + ltw + the AjcTestCase classpath (ie usually aspectjrt, junit, and + * testing infra) + *

    + * Element "" and "" can be used. For now a full match is performed on the output of + * the runned target only (not the whole Ant invocation). This is experimental and advised to use a "" task instead or a + * "" whose main that throws some exception upon failure. + * + * + * @author Alexandre Vasseur + */ +public class AntSpec implements ITestStep { + + // ALSO SEE AJC + private final static String DEFAULT_LTW_CLASSPATH_ENTRIES = ".." + File.separator + "asm/bin" + File.pathSeparator + ".." + + File.separator + "bridge/bin" + File.pathSeparator + ".." + File.separator + "loadtime/bin" + File.pathSeparator + + ".." + File.separator + "loadtime5/bin" + File.pathSeparator + ".." + File.separator + "weaver/bin" + + File.pathSeparator + ".." + File.separator + "org.aspectj.matcher/bin" + File.pathSeparator + ".." + File.separator + + "lib/bcel/bcel.jar" + File.pathSeparator + ".." + File.separator + "lib/bcel/bcel-verifier.jar";; + + private boolean m_verbose = false; + private AjcTest m_ajcTest; + private OutputSpec m_stdErrSpec; + private OutputSpec m_stdOutSpec; + private String m_antFile; + private String m_antTarget; + + public void execute(final AjcTestCase inTestCase) { + final String failMessage = "test \"" + m_ajcTest.getTitle() + "\" failed: "; + + File buildFile = new File(m_ajcTest.getDir() + File.separatorChar + m_antFile); + if (!buildFile.exists()) { + AjcTestCase.fail(failMessage + "no such Ant file " + buildFile.getAbsolutePath()); + } + Project p = new Project(); + final StringBuffer stdout = new StringBuffer(); + final StringBuffer stderr = new StringBuffer(); + final StringBuffer verboseLog = new StringBuffer(); + try { + // read the Ant file + p.init(); + p.setUserProperty("ant.file", buildFile.getAbsolutePath()); + // setup aj.sandbox + p.setUserProperty("aj.sandbox", inTestCase.getSandboxDirectory().getAbsolutePath()); + // setup aj.dir "modules" folder + p.setUserProperty("aj.root", new File("..").getAbsolutePath()); + + // create the test implicit path aj.path that contains the sandbox + regular test infra path + Path path = new Path(p, inTestCase.getSandboxDirectory().getAbsolutePath()); + populatePath(path, DEFAULT_LTW_CLASSPATH_ENTRIES); + populatePath(path, AjcTestCase.DEFAULT_CLASSPATH_ENTRIES); + p.addReference("aj.path", path); + p.setBasedir(buildFile.getAbsoluteFile().getParent()); + ProjectHelper helper = ProjectHelper.getProjectHelper(); + helper.parse(p, buildFile); + + // use default target if no target specified + if (m_antTarget == null) { + m_antTarget = p.getDefaultTarget(); + } + + // make sure we listen for failure + DefaultLogger consoleLogger = new DefaultLogger() { + public void buildFinished(BuildEvent event) { + super.buildFinished(event); + if (event.getException() != null) { + + try { + File antout = new File(inTestCase.getSandboxDirectory().getAbsolutePath(), "antout"); + if (antout.exists()) { + stdout.append("Forked java command stdout:\n"); + System.out.println("Forked java command stdout:"); + FileReader fr = new FileReader(antout); + BufferedReader br = new BufferedReader(fr); + String line = br.readLine(); + while (line != null) { + stdout.append(line).append("\n"); + System.out.println(stdout); + line = br.readLine(); + } + fr.close(); + } + + File anterr = new File(inTestCase.getSandboxDirectory().getAbsolutePath(), "anterr"); + if (anterr.exists()) { + stdout.append("Forked java command stderr:\n"); + System.out.println("Forked java command stderr:"); + FileReader fr = new FileReader(anterr); + BufferedReader br = new BufferedReader(fr); + String line = br.readLine(); + while (line != null) { + stdout.append(line).append("\n"); + System.out.println(stdout); + line = br.readLine(); + } + fr.close(); + } + } catch (Exception e) { + System.out.println("Exception whilst loading forked java task output " + e.getMessage() + "\n"); + e.printStackTrace(); + stdout.append("Exception whilst loading forked java task output " + e.getMessage() + "\n"); + } + + StringBuffer message = new StringBuffer(); + message.append(event.getException().toString()).append("\n"); + message.append(verboseLog); + message.append(stdout); + message.append(stderr); + // AjcTestCase.fail(failMessage + "failure " + event.getException()); + AjcTestCase.fail(message.toString()); + } + } + + public void targetFinished(BuildEvent event) { + super.targetFinished(event); + if (event.getException() != null) { + AjcTestCase.fail(failMessage + "failure in '" + event.getTarget() + "' " + event.getException()); + } + } + + public void messageLogged(BuildEvent event) { + super.messageLogged(event); + + Target target = event.getTarget(); + if (target != null && m_antTarget.equals(target.getName()) && event.getSource() instanceof Java) + switch (event.getPriority()) { + case Project.MSG_INFO: + stdout.append(event.getMessage()).append('\n'); + break; + case Project.MSG_WARN: + stderr.append(event.getMessage()).append('\n'); + break; + case Project.MSG_VERBOSE: + verboseLog.append(event.getMessage()).append('\n'); + break; + } + } + }; + consoleLogger.setErrorPrintStream(System.err); + consoleLogger.setOutputPrintStream(System.out); + consoleLogger.setMessageOutputLevel(m_verbose ? Project.MSG_VERBOSE : Project.MSG_ERR); + p.addBuildListener(consoleLogger); + } catch (Throwable t) { + AjcTestCase.fail(failMessage + "invalid Ant script :" + t.toString()); + } + try { + p.setProperty("verbose", "true"); + p.fireBuildStarted(); + p.executeTarget(m_antTarget); + p.fireBuildFinished(null); + } catch (BuildException e) { + p.fireBuildFinished(e); + } catch (Throwable t) { + AjcTestCase.fail(failMessage + "error when invoking target :" + t.toString()); + } + + /* See if stdout/stderr matches test specification */ + if (m_stdOutSpec != null) { + m_stdOutSpec.matchAgainst(stdout.toString()); + } + if (m_stdErrSpec != null) { + String stderr2 = stderr.toString(); + // Working around this rediculous message that still comes out of Java7 builds: + if (stderr2.indexOf("Class JavaLaunchHelper is implemented in both")!=-1 && stderr2.indexOf('\n')!=-1) { + stderr2 = stderr2.replaceAll("objc\\[[0-9]*\\]: Class JavaLaunchHelper is implemented in both [^\n]*\n",""); + } + // JDK 11 is complaining about illegal reflective calls - temporary measure ignore these - does that get all tests passing and this is the last problem? + if (stderr2.indexOf("WARNING: Illegal reflective access using Lookup on org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor") != -1) { +// WARNING: An illegal reflective access operation has occurred +// WARNING: Illegal reflective access using Lookup on org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor (file:/Users/aclement/gits/org.aspectj/loadtime/bin/) to class java.lang.ClassLoader +// WARNING: Please consider reporting this to the maintainers of org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor +// WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations +// WARNING: All illegal access operations will be denied in a future release + + stderr2 = stderr2.replaceAll("WARNING: An illegal reflective access operation has occurred\n",""); + stderr2 = stderr2.replaceAll("WARNING: Illegal reflective access using Lookup on org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor[^\n]*\n",""); + stderr2 = stderr2.replaceAll("WARNING: Please consider reporting this to the maintainers of org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor\n",""); + stderr2 = stderr2.replaceAll("WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations\n",""); + stderr2 = stderr2.replaceAll("WARNING: All illegal access operations will be denied in a future release\n",""); + } + + m_stdErrSpec.matchAgainst(stderr2); + } + } + + public void addStdErrSpec(OutputSpec spec) { + if (m_stdErrSpec != null) + throw new UnsupportedOperationException("only one 'stderr' allowed in 'ant'"); + m_stdErrSpec = spec; + } + + public void addStdOutSpec(OutputSpec spec) { + if (m_stdOutSpec != null) + throw new UnsupportedOperationException("only one 'stdout' allowed in 'ant'"); + m_stdOutSpec = spec; + } + + public void setVerbose(String verbose) { + if (verbose != null && "true".equalsIgnoreCase(verbose)) { + m_verbose = true; + } + } + + public void setFile(String file) { + m_antFile = file; + } + + public void setTarget(String target) { + m_antTarget = target; + } + + public void addExpectedMessage(ExpectedMessageSpec message) { + throw new UnsupportedOperationException("don't use 'message' in 'ant' specs."); + } + + public void setBaseDir(String dir) { + ; + } + + public void setTest(AjcTest test) { + m_ajcTest = test; + } + + private static void populatePath(Path path, String pathEntries) { + StringTokenizer st = new StringTokenizer(pathEntries, File.pathSeparator); + while (st.hasMoreTokens()) { + path.setPath(new File(st.nextToken()).getAbsolutePath()); + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/AutowiredXMLBasedAjcTestCase.java b/testing/src/test/java/org/aspectj/testing/AutowiredXMLBasedAjcTestCase.java new file mode 100644 index 000000000..467d9f236 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/AutowiredXMLBasedAjcTestCase.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2005 Contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * initial implementation Alexandre Vasseur + *******************************************************************************/ +package org.aspectj.testing; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import junit.extensions.TestSetup; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Map; +import java.util.HashMap; +import java.io.InputStreamReader; +import java.io.FileInputStream; + +import org.apache.commons.digester.Digester; +import org.aspectj.tools.ajc.Ajc; + +/** + * Autowiring of XML test spec file as JUnit tests. + *

    + * Extend this class and implement the getSpecFile and the static suite() method. + * All tests described in the XML spec file will be auto-registered as JUnit tests. + * Any regular test() method will be registered as well. + * + * @author Alexandre Vasseur + */ +public abstract class AutowiredXMLBasedAjcTestCase extends XMLBasedAjcTestCase { + + private Map testMap = new HashMap(); + + public void addTest(AjcTest test) { + testMap.put(test.getTitle(), test); + } + + /* + * Return a map from (String) test title -> AjcTest + */ + protected Map getSuiteTests() { + return testMap; + } + + public static Test loadSuite(Class testCaseClass) { + TestSuite suite = new TestSuite(testCaseClass.getName()); + //suite.addTestSuite(testCaseClass); + + // wire the spec file + try { + final AutowiredXMLBasedAjcTestCase wired = (AutowiredXMLBasedAjcTestCase) testCaseClass.newInstance(); + System.out.println("LOADING SUITE: " + wired.getSpecFile().getPath()); + Digester d = wired.getDigester(); + try { + InputStreamReader isr = new InputStreamReader(new FileInputStream(wired.getSpecFile())); + d.parse(isr); + } catch (Exception ex) { + fail("Unable to load suite " + wired.getSpecFile().getPath() + " : " + ex); + } + wired.ajc = new Ajc(); + + Map ajTests = wired.getSuiteTests(); + + for (Iterator> iterator = ajTests.entrySet().iterator(); iterator.hasNext();) { + final Map.Entry entry = iterator.next(); + + suite.addTest( + new TestCase(entry.getKey().toString()) { + + protected void runTest() { + entry.getValue().runTest(wired); + } + + public String getName() { + return entry.getKey(); + } + } + ); + } + } catch (Throwable t) { + final String message = t.toString(); + suite.addTest( + new TestCase("error") { + protected void runTest() { + fail(message); + } + } + ); + } + + // wire the test methods as well if any + // this simple check avoids failure when no test.. method is found. + // it could be refined to lookup in the hierarchy as well, and excluding private method as JUnit does. + Method[] testMethods = testCaseClass.getDeclaredMethods(); + for (int i = 0; i < testMethods.length; i++) { + Method testMethod = testMethods[i]; + if (testMethod.getName().startsWith("test")) { + suite.addTestSuite(testCaseClass); + break; + } + } + + TestSetup wrapper = new TestSetup(suite) { + /* (non-Javadoc) + * @see junit.extensions.TestSetup#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + //suiteLoaded = false; + } + /* (non-Javadoc) + * @see junit.extensions.TestSetup#tearDown() + */ + protected void tearDown() throws Exception { + super.tearDown(); + //suiteLoaded = false; + } + }; + return wrapper; + + //return suite; + } + + /* (non-Javadoc) + * @see org.aspectj.tools.ajc.AjcTestCase#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + } + + /** + * This helper method runs the test with the given title in the + * suite spec file. All tests steps in given ajc-test execute + * in the same sandbox. + */ + protected void runTest(String title) { + AjcTest currentTest = (AjcTest) testMap.get(title); + if (currentTest == null) { + fail("No test '" + title + "' in suite."); + } + ajc.setShouldEmptySandbox(true); + currentTest.runTest(this); + } + + + +} diff --git a/testing/src/test/java/org/aspectj/testing/CompileSpec.java b/testing/src/test/java/org/aspectj/testing/CompileSpec.java new file mode 100644 index 000000000..22570e3d1 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/CompileSpec.java @@ -0,0 +1,336 @@ +/* ******************************************************************* + * Copyright (c) 2004,2016 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import org.aspectj.testing.util.TestUtil; +import org.aspectj.tools.ajc.AjcTestCase; +import org.aspectj.tools.ajc.CompilationResult; + +/** + * @author Adrian Colyer + * @author Andy Clement + */ +public class CompileSpec implements ITestStep { + + private List expected = new ArrayList(); + + private String files; + private boolean includeClassesDir; + private String aspectpath; + private String classpath; + private String modulepath; + private String inpath; + private String sourceroots; + private String outjar; + private String outxml; + private String xlintfile; + private String options; + private String baseDir; + private String extdirs; + private AjcTest myTest; + + public CompileSpec() { + } + + public void execute(AjcTestCase inTestCase) { + File base = new File(baseDir); + String[] args = buildArgs(); + CompilationResult result = inTestCase.ajc(base,args); + AjcTestCase.MessageSpec messageSpec = buildMessageSpec(); + String failMessage = "test \"" + myTest.getTitle() + "\" failed"; + inTestCase.assertMessages(result,failMessage,messageSpec); + inTestCase.setShouldEmptySandbox(false); // so subsequent steps in same test see my results + } + + public void addExpectedMessage(ExpectedMessageSpec message) { + expected.add(message); + } + + public void setBaseDir(String dir) { + this.baseDir = dir; + } + + protected String getBaseDir() { return baseDir; } + + public void setTest(AjcTest t) { + this.myTest = t; + if (options != null && (options.indexOf("-1.5") != -1)) { + myTest.setVm("1.5"); + } + } + + protected AjcTest getTest() { return myTest; } + + /** + * @return Returns the aspectpath. + */ + public String getAspectpath() { + return aspectpath; + } + /** + * @param aspectpath The aspectpath to set. + */ + public void setAspectpath(String aspectpath) { + this.aspectpath = aspectpath.replace(',',File.pathSeparatorChar); + } + /** + * @return Returns the classpath. + */ + public String getClasspath() { + return classpath; + } + /** + * @param classpath The classpath to set. + */ + public void setClasspath(String classpath) { + this.classpath = classpath.replace(',',File.pathSeparatorChar); + } + + public String getModulepath() { + return this.modulepath; + } + + public void setModulepath(String modulepath) { + this.modulepath = modulepath.replace(',', File.pathSeparatorChar); + } + /** + * @return Returns the files. + */ + public String getFiles() { + return files; + } + /** + * @param files The files to set. + */ + public void setFiles(String files) { + this.files = files; + } + /** + * @return Returns the includeClassesDir. + */ + public boolean isIncludeClassesDir() { + return includeClassesDir; + } + /** + * @param includeClassesDir The includeClassesDir to set. + */ + public void setIncludeClassesDir(boolean includeClassesDir) { + this.includeClassesDir = includeClassesDir; + } + /** + * @return Returns the inpath. + */ + public String getInpath() { + return inpath; + } + /** + * @param inpath The inpath to set. + */ + public void setInpath(String inpath) { + this.inpath = inpath.replace(',',File.pathSeparatorChar).replace(';',File.pathSeparatorChar); + } + /** + * @return Returns the options. + */ + public String getOptions() { + return options; + } + /** + * @param options The options to set. + */ + public void setOptions(String options) { + int i = options.indexOf("!eclipse"); + if (i != -1) { + this.options = options.substring(0,i); + this.options += options.substring(i + "!eclipse".length()); + } else { + this.options = options; + } + } + /** + * @return Returns the outjar. + */ + public String getOutjar() { + return outjar; + } + /** + * @param outjar The outjar to set. + */ + public void setOutjar(String outjar) { + this.outjar = outjar; + } + + /** + * @return Returns the outxml. + */ + public String getOutxmlfile() { + return outxml; + } + + /** + * @param outxml The the of the aop.xml file to generate + */ + public void setOutxmlfile(String outxml) { + this.outxml = outxml; + } + /** + * @return Returns the sourceroots. + */ + public String getSourceroots() { + return sourceroots; + } + /** + * @param sourceroots The sourceroots to set. + */ + public void setSourceroots(String sourceroots) { + this.sourceroots = sourceroots; + } + /** + * @return Returns the xlintfile. + */ + public String getXlintfile() { + return xlintfile; + } + /** + * @param xlintfile The xlintfile to set. + */ + public void setXlintfile(String xlintfile) { + this.xlintfile = xlintfile; + } + + public String getExtdirs() { return extdirs;} + public void setExtdirs(String extdirs) { this.extdirs = extdirs; } + + protected String[] buildArgs() { + StringBuffer args = new StringBuffer(); + // add any set options, and then files to compile at the end + if (getAspectpath() != null) { + args.append("-aspectpath "); + args.append(getAspectpath()); + args.append(" "); + } + if (getSourceroots() != null) { + args.append("-sourceroots "); + args.append(getSourceroots()); + args.append(" "); + } + if (getOutjar() != null) { + args.append("-outjar "); + args.append(getOutjar()); + args.append(" "); + } + if (getOutxmlfile() != null) { + args.append("-outxmlfile "); + args.append(getOutxmlfile()); + args.append(" "); + } + if (getOptions() != null) { + StringTokenizer strTok = new StringTokenizer(getOptions(),","); + while (strTok.hasMoreTokens()) { + // For an option containing a comma, pass in a { in its place + args.append(strTok.nextToken().replace('{', ',')); + args.append(" "); + } + } + if (getClasspath() != null) { + args.append("-classpath "); + args.append(getClasspath()); + args.append(" "); + } + if (getModulepath() != null) { + args.append("-p "); + args.append(rewrite(getModulepath())); + args.append(" "); + } + if (getXlintfile() != null) { + args.append("-Xlintfile "); + args.append(getXlintfile()); + args.append(" "); + } + if (getExtdirs() != null) { + args.append("-extdirs "); + args.append(getExtdirs()); + args.append(" "); + } + List fileList = new ArrayList(); + List jarList = new ArrayList(); + // convention that any jar on file list should be added to inpath + String files = getFiles(); + if (files == null) files = ""; + StringTokenizer strTok = new StringTokenizer(files,","); + while (strTok.hasMoreTokens()) { + final String file = strTok.nextToken(); + if (file.endsWith(".jar")) { + jarList.add(file); + } else { + fileList.add(file); + } + } + if ((getInpath() != null) || !jarList.isEmpty()) { + args.append("-inpath "); + if (getInpath() != null) args.append(getInpath()); + for (String jar: jarList) { + args.append(File.pathSeparator); + args.append(jar); + } + args.append(" "); + } + for (String file: fileList) { + args.append(file); + args.append(" "); + } + String argumentString = args.toString(); + strTok = new StringTokenizer(argumentString," "); + String[] ret = new String[strTok.countTokens()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = strTok.nextToken(); + } + return ret; + } + + private String rewrite(String path) { + path = path.replace("$runtime", TestUtil.aspectjrtPath().toString()); + return path; + } + + protected AjcTestCase.MessageSpec buildMessageSpec() { + List infos = null; + List warnings = new ArrayList(); + List errors = new ArrayList(); + List fails = new ArrayList(); + List weaveInfos = new ArrayList(); + for (ExpectedMessageSpec exMsg: expected) { + String kind = exMsg.getKind(); + if (kind.equals("info")) { + if (infos == null) infos = new ArrayList(); + infos.add(exMsg.toMessage()); + } else if (kind.equals("warning")) { + warnings.add(exMsg.toMessage()); + } else if (kind.equals("error")) { + errors.add(exMsg.toMessage()); + } else if (kind.equals("fail")) { + fails.add(exMsg.toMessage()); + } else if (kind.equals("abort")) { + fails.add(exMsg.toMessage()); + } else if (kind.equals("weave")) { + weaveInfos.add(exMsg.toMessage()); + } + } + return new AjcTestCase.MessageSpec(infos,warnings,errors,fails,weaveInfos); + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/ExpectedMessageSpec.java b/testing/src/test/java/org/aspectj/testing/ExpectedMessageSpec.java new file mode 100644 index 000000000..2193b55ae --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/ExpectedMessageSpec.java @@ -0,0 +1,94 @@ +/* ******************************************************************* + * Copyright (c) 2004 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; + +import org.aspectj.tools.ajc.AjcTestCase; + +/** + * @author colyer + * + * TODO To change the template for this generated type comment go to + * Window - Preferences - Java - Code Style - Code Templates + */ +public class ExpectedMessageSpec { + + private String kind = "error"; + private int line = -1; + private String text; + private String file; + private String details; + + public AjcTestCase.Message toMessage() { + return new AjcTestCase.Message(line,file,text,null); + } + + /** + * @return Returns the details. + */ + public String getDetails() { + return details; + } + /** + * @param details The details to set. + */ + public void setDetails(String details) { + this.details = details; + } + /** + * @return Returns the file. + */ + public String getFile() { + return file; + } + /** + * @param file The file to set. + */ + public void setFile(String file) { + this.file = file; + } + /** + * @return Returns the kind. + */ + public String getKind() { + return kind; + } + /** + * @param kind The kind to set. + */ + public void setKind(String kind) { + this.kind = kind; + } + /** + * @return Returns the line. + */ + public int getLine() { + return line; + } + /** + * @param line The line to set. + */ + public void setLine(int line) { + this.line = line; + } + /** + * @return Returns the text. + */ + public String getText() { + return text; + } + /** + * @param text The text to set. + */ + public void setText(String text) { + this.text = text; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/FileSpec.java b/testing/src/test/java/org/aspectj/testing/FileSpec.java new file mode 100644 index 000000000..2164f38f1 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/FileSpec.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2010 Contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Andy Clement - SpringSource + *******************************************************************************/ +package org.aspectj.testing; + +import java.io.File; + +import org.aspectj.tools.ajc.AjcTestCase; + +/** + * Support simple file system operations in a test spec. Example:
    + * <file deletefile="foo.jar"/> will delete the file foo.jar from the sandbox. + * + * @author Andy Clement + */ +public class FileSpec implements ITestStep { + + private String toDelete; + + private String renameFrom; + private String renameTo; + + // private String dir; + // private AjcTest test; + + public FileSpec() { + } + + public void setRenameFrom(String file) { + this.renameFrom = file; + } + + public void setRenameTo(String file) { + this.renameTo = file; + } + + public void setDeletefile(String file) { + this.toDelete = file; + } + + @Override + public void addExpectedMessage(ExpectedMessageSpec message) { + } + + @Override + public void execute(AjcTestCase inTestCase) { + File sandbox = inTestCase.getSandboxDirectory(); + if (toDelete != null) { + File targetForDeletion = new File(sandbox, toDelete); + if (targetForDeletion.isFile()) { + targetForDeletion.delete(); + } else { + recursiveDelete(targetForDeletion); + } + } + if (renameFrom != null) { + if (renameTo == null) { + throw new IllegalStateException("If setting renameFrom the renameTo should also be set"); + } + File fileFrom = new File(sandbox, renameFrom); + File fileTo = new File(sandbox, renameTo); + fileFrom.renameTo(fileTo); + } + } + + private void recursiveDelete(File toDelete) { + if (toDelete.isDirectory()) { + File[] files = toDelete.listFiles(); + for (File f: files) { + recursiveDelete(f); + } + } + toDelete.delete(); + } + + @Override + public void setBaseDir(String dir) { + // this.dir = dir; + } + + @Override + public void setTest(AjcTest test) { + // this.test = test; + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/ITestStep.java b/testing/src/test/java/org/aspectj/testing/ITestStep.java new file mode 100644 index 000000000..673d6c66d --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/ITestStep.java @@ -0,0 +1,28 @@ +/* ******************************************************************* + * Copyright (c) 2004 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; + +import org.aspectj.tools.ajc.AjcTestCase; + +/** + * @author Adrian Colyer + */ +public interface ITestStep { + + void execute(AjcTestCase inTestCase); + + void addExpectedMessage(ExpectedMessageSpec message); + + void setBaseDir(String dir); + + void setTest(AjcTest test); +} diff --git a/testing/src/test/java/org/aspectj/testing/MakeTestClass.java b/testing/src/test/java/org/aspectj/testing/MakeTestClass.java new file mode 100644 index 000000000..721cb623b --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/MakeTestClass.java @@ -0,0 +1,118 @@ +/* + * Created on 02-Aug-2004 + * + */ +package org.aspectj.testing; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.digester.Digester; + +/** + * @author colyer + * + * TODO To change the template for this generated type comment go to + * Window - Preferences - Java - Code Style - Code Templates + */ +public class MakeTestClass { + + private static final String HEADER = + "/* *******************************************************************\n" + + " * Copyright (c) 2004 IBM Corporation\n" + + " * All rights reserved.\n" + + " * This program and the accompanying materials are made available\n" + + " * under the terms of the Eclipse Public License v1.0\n" + + " * which accompanies this distribution and is available at\n" + + " * http://www.eclipse.org/legal/epl-v10.html \n" + + " * \n" + + " * ******************************************************************/\n" + + "package org.aspectj.systemtest.XXX;\n" + + "\n" + + "import java.io.File;\n" + + "import junit.framework.Test;\n" + + "import org.aspectj.testing.XMLBasedAjcTestCase;\n" + + "\n" + + "public class "; + + private static final String BODY_1 = + " extends org.aspectj.testing.XMLBasedAjcTestCase {\n" + + "\n" + + " public static Test suite() {\n" + + " return XMLBasedAjcTestCase.loadSuite("; + + private static final String BODY_2 = + ".class);\n" + + " }\n" + + "\n" + + " protected File getSpecFile() {\n" + + " return new File(\""; + + private static final String BODY_3 = + "\");\n" + + " }\n"; + + private static final String FOOTER = + "}\n"; + + private List tests = new ArrayList(); + private String className; + private String suiteFile; + + public static void main(String[] args) throws Exception { + new MakeTestClass(args[0],args[1]).makeTestClass(); + } + + public MakeTestClass(String className, String suiteFile)throws Exception { + this.className = className; + this.suiteFile = suiteFile; + Digester d = getDigester(); + InputStreamReader isr = new InputStreamReader(new FileInputStream(suiteFile)); + d.parse(isr); + } + + public void addTest(AjcTest test) { + tests.add(test); + } + + public void makeTestClass() throws Exception { + FileOutputStream fos = new FileOutputStream(className + ".java"); + PrintStream out = new PrintStream(fos); + out.print(HEADER); + out.print(className); + out.print(BODY_1); + out.print(className); + out.print(BODY_2); + out.print(suiteFile); + out.print(BODY_3); + out.println(); + int testNo = 1; + NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumIntegerDigits(3); + for (AjcTest test: tests) { + out.println(); + out.print(" public void test"); + out.print(nf.format(testNo++)); + out.println("(){"); + out.println(" runTest(\"" + test.getTitle() + "\");"); + out.println(" }"); + } + out.println(); + out.println(FOOTER); + out.close(); + } + + private Digester getDigester() { + Digester digester = new Digester(); + digester.push(this); + digester.addObjectCreate("suite/ajc-test",AjcTest.class); + digester.addSetProperties("suite/ajc-test"); + digester.addSetNext("suite/ajc-test","addTest","org.aspectj.testing.AjcTest"); + return digester; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/OutputLine.java b/testing/src/test/java/org/aspectj/testing/OutputLine.java new file mode 100644 index 000000000..fc6fe4ce9 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/OutputLine.java @@ -0,0 +1,42 @@ +/* ******************************************************************* + * Copyright (c) 2005 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; + +/** + * @author Adrian Colyer + * @author Andy Clement + */ +public class OutputLine { + + // Expected text + private String text; + + // Comma separated list of vm versions on which this is expected + private String vm; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getVm() { + return vm; + } + + public void setVm(String vm) { + this.vm = vm; + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/OutputSpec.java b/testing/src/test/java/org/aspectj/testing/OutputSpec.java new file mode 100644 index 000000000..9eab46098 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/OutputSpec.java @@ -0,0 +1,134 @@ +/* ******************************************************************* + * Copyright (c) 2005 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.StringTokenizer; + +import org.aspectj.tools.ajc.AjcTestCase; +import org.aspectj.util.LangUtil; + +public class OutputSpec { + + private List expectedOutputLines = new ArrayList(); + + public void addLine(OutputLine line) { + if (line.getVm() == null || matchesThisVm(line.getVm())) { + expectedOutputLines.add(line.getText()); + } + } + + /** + * For a test output line that has specified a vm version, check if it matches the vm we are running on. + * vm might be "1.2,1.3,1.4,1.5" or simply "9" or it may be a version with a '+' suffix indicating that + * level or later, e.g. "9+" should be ok on Java 10 + * @return true if the current vm version matches the spec + */ + private boolean matchesThisVm(String vm) { + // vm might be 1.2, 1.3, 1.4, 1.5 or 1.9 possibly with a '+' in there + // For now assume + is attached to there only being one version, like "9+" + if (vm.contains(LangUtil.getVmVersionString())) { + return true; + } + if (vm.endsWith("+")) { + double vmVersion = LangUtil.getVmVersion(); + double vmSpecified = Double.parseDouble(vm.substring(0,vm.length()-1)); + return vmVersion >= vmSpecified; + } + return false; + } + + public void matchAgainst(String output) { + matchAgainst(output, "yes"); + } + + public void matchAgainst(String output, String ordered) { + if (ordered != null && ordered.equals("no")) { + unorderedMatchAgainst(output); + return; + } + boolean matches = false; + int lineNo = 0; + StringTokenizer strTok = new StringTokenizer(output,"\n"); + if (strTok.countTokens() == expectedOutputLines.size()) { + matches = true; + for (String line: expectedOutputLines) { + lineNo++; + String outputLine = strTok.nextToken().trim(); + /* Avoid trying to match on ajSandbox source names that appear in messages */ + if (outputLine.indexOf(line) == -1) { + matches = false; + break; + } + } + } else { lineNo = -1; } + if (!matches) { + createFailureMessage(output, lineNo, strTok.countTokens()); + } + } + + public void unorderedMatchAgainst(String output) { + List outputFound = getOutputFound(output); + if(outputFound.size() != expectedOutputLines.size()) { + createFailureMessage(output, -1, outputFound.size()); + return; + } + List expected = new ArrayList(); + expected.addAll(expectedOutputLines); + List found = new ArrayList(); + found.addAll(outputFound); + for (Iterator iterator = outputFound.iterator(); iterator.hasNext();) { + String lineFound = (String) iterator.next(); + for (Iterator iterator2 = expectedOutputLines.iterator(); iterator2.hasNext();) { + String lineExpected = (String) iterator2.next(); + if (lineFound.indexOf(lineExpected)!= -1) { + found.remove(lineFound); + expected.remove(lineExpected); + continue; + } + } + } + if (!found.isEmpty() || !expected.isEmpty()) { + createFailureMessage(output,-2,outputFound.size()); + } + } + + private void createFailureMessage(String output, int lineNo, int sizeFound) { + StringBuffer failMessage = new StringBuffer(); + failMessage.append("\n expecting output:\n"); + for (String line: expectedOutputLines) { + failMessage.append(line); + failMessage.append("\n"); + } + failMessage.append(" but found output:\n"); + failMessage.append(output); + failMessage.append("\n"); + if (lineNo==-1) { + failMessage.append("Expected "+expectedOutputLines.size()+" lines of output but there are "+sizeFound); + } else if (lineNo >= 0) { + failMessage.append("First difference is on line " + lineNo); + } + failMessage.append("\n"); + AjcTestCase.fail(failMessage.toString()); + } + + private List getOutputFound(String output) { + List found = new ArrayList(); + StringTokenizer strTok = new StringTokenizer(output,"\n"); + while(strTok.hasMoreTokens()) { + String outputLine = strTok.nextToken().trim(); + found.add(outputLine); + } + return found; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/RunSpec.java b/testing/src/test/java/org/aspectj/testing/RunSpec.java new file mode 100644 index 000000000..62b93bcb0 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/RunSpec.java @@ -0,0 +1,262 @@ +/* ******************************************************************* + * Copyright (c) 2004 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, Abraham Nevado (lucierna) + * ******************************************************************/ +package org.aspectj.testing; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +import org.aspectj.tools.ajc.AjcTestCase; +import org.aspectj.util.FileUtil; + +/** + * @author Adrian Colyer + */ +public class RunSpec implements ITestStep { + + private List expected = new ArrayList(); + private String classToRun; + private String moduleToRun; // alternative to classToRun on JDK9+ + private String baseDir; + private String options; + private String cpath; + private String mpath; + private String orderedStderr; + private AjcTest myTest; + private OutputSpec stdErrSpec; + private OutputSpec stdOutSpec; + private String ltwFile; + private String xlintFile; + private String vmargs; + private String usefullltw; + + @Override + public String toString() { + return "RunSpec: Running '"+classToRun+"' in directory '"+baseDir+"'. Classpath of '"+cpath+"'"; + } + public RunSpec() { + } + + @Override + public void execute(AjcTestCase inTestCase) { + if (!expected.isEmpty()) { + System.err.println("Warning, message spec for run command is currently ignored (org.aspectj.testing.RunSpec)"); + } + String[] args = buildArgs(); +// System.err.println("? execute() inTestCase='" + inTestCase + "', ltwFile=" + ltwFile); + boolean useLtw = copyLtwFile(inTestCase.getSandboxDirectory()); + + copyXlintFile(inTestCase.getSandboxDirectory()); + try { + setSystemProperty("test.base.dir", inTestCase.getSandboxDirectory().getAbsolutePath()); + + AjcTestCase.RunResult rr = inTestCase.run(getClassToRun(), getModuleToRun(), args, vmargs, getClasspath(), getModulepath(), useLtw, "true".equalsIgnoreCase(usefullltw)); + + if (stdErrSpec != null) { + stdErrSpec.matchAgainst(rr.getStdErr(), orderedStderr); + } + if (stdOutSpec != null) { + stdOutSpec.matchAgainst(rr.getStdOut()); + } + } finally { + restoreProperties(); + } + } + + /* + * Logic to save/restore system properties. Copied from LTWTests. As Matthew noted, need to refactor LTWTests to use this + */ + + private Properties savedProperties = new Properties(); + + public void setSystemProperty(String key, String value) { + Properties systemProperties = System.getProperties(); + copyProperty(key, systemProperties, savedProperties); + systemProperties.setProperty(key, value); + } + + private static void copyProperty(String key, Properties from, Properties to) { + String value = from.getProperty(key, NULL); + to.setProperty(key, value); + } + + private final static String NULL = "null"; + + protected void restoreProperties() { + Properties systemProperties = System.getProperties(); + for (Enumeration enu = savedProperties.keys(); enu.hasMoreElements();) { + String key = (String) enu.nextElement(); + String value = savedProperties.getProperty(key); + if (value == NULL) + systemProperties.remove(key); + else + systemProperties.setProperty(key, value); + } + } + + @Override + public void addExpectedMessage(ExpectedMessageSpec message) { + expected.add(message); + } + + @Override + public void setBaseDir(String dir) { + this.baseDir = dir; + } + + @Override + public void setTest(AjcTest test) { + this.myTest = test; + } + + public AjcTest getTest() { + return this.myTest; + } + + public String getOptions() { + return options; + } + + public void setOptions(String options) { + this.options = options; + } + + public String getClasspath() { + if (cpath == null) + return null; + return this.cpath.replace('/', File.separatorChar).replace(',', File.pathSeparatorChar); + } + + public String getModulepath() { + if (mpath == null) + return null; + return this.mpath.replace('/', File.separatorChar).replace(',', File.pathSeparatorChar); + } + + public void setModulepath(String mpath) { + this.mpath = mpath; + } + + public void setClasspath(String cpath) { + this.cpath = cpath; + } + + public void addStdErrSpec(OutputSpec spec) { + this.stdErrSpec = spec; + } + + public void addStdOutSpec(OutputSpec spec) { + this.stdOutSpec = spec; + } + + public void setOrderedStderr(String orderedStderr) { + this.orderedStderr = orderedStderr; + } + + public String getClassToRun() { + return classToRun; + } + + public void setClassToRun(String classToRun) { + this.classToRun = classToRun; + } + + public void setModuleToRun(String moduleToRun) { + this.moduleToRun = moduleToRun; + } + + public String getModuleToRun() { + return this.moduleToRun; + } + + public String getLtwFile() { + return ltwFile; + } + + public void setLtwFile(String ltwFile) { + this.ltwFile = ltwFile; + } + + private String[] buildArgs() { + if (options == null) + return new String[0]; + StringTokenizer strTok = new StringTokenizer(options, ","); + String[] ret = new String[strTok.countTokens()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = strTok.nextToken(); + } + return ret; + } + + private boolean copyLtwFile(File sandboxDirectory) { + boolean useLtw = false; + + if (ltwFile != null) { + // TODO maw use flag rather than empty file name + if (ltwFile.trim().length() == 0) + return true; + + File from = new File(baseDir, ltwFile); + File to = new File(sandboxDirectory, "META-INF" + File.separator + "aop.xml"); + // System.out.println("RunSpec.copyLtwFile() from=" + from.getAbsolutePath() + " to=" + to.getAbsolutePath()); + try { + FileUtil.copyFile(from, to); + useLtw = true; + } catch (IOException ex) { + AjcTestCase.fail(ex.toString()); + } + } + + return useLtw; + } + + public String getXlintFile() { + return xlintFile; + } + + public void setXlintFile(String xlintFile) { + this.xlintFile = xlintFile; + } + + public void setVmargs(String vmargs) { + this.vmargs = vmargs; + } + + public String getVmargs() { + return vmargs; + } + + + public String getUsefullltw() { + return usefullltw; + } + + public void setUsefullltw(String usefullltw) { + this.usefullltw = usefullltw; + } + + private void copyXlintFile(File sandboxDirectory) { + if (xlintFile != null) { + File from = new File(baseDir, xlintFile); + File to = new File(sandboxDirectory, File.separator + xlintFile); + try { + FileUtil.copyFile(from, to); + } catch (IOException ex) { + AjcTestCase.fail(ex.toString()); + } + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/TestingModuleTests.java b/testing/src/test/java/org/aspectj/testing/TestingModuleTests.java deleted file mode 100644 index b6c20b215..000000000 --- a/testing/src/test/java/org/aspectj/testing/TestingModuleTests.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.aspectj.testing; -/* ******************************************************************* - * 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 v1.0 - * which accompanies this distribution and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Xerox/PARC initial implementation - * ******************************************************************/ - - -// default package - -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; - -public class TestingModuleTests extends TestCase { - - public static Test suite() { - TestSuite suite = new TestSuite(TestingModuleTests.class.getName()); - suite.addTest(org.aspectj.testing.harness.bridge.TestingBridgeTests.suite()); - suite.addTest(org.aspectj.testing.taskdefs.TaskdefTests.suite()); - suite.addTest(org.aspectj.testing.util.UtilTests.suite()); - suite.addTest(org.aspectj.testing.util.options.OptionsTests.suite()); - suite.addTest(org.aspectj.testing.xml.TestingXmlTests.suite()); - return suite; - } - - public TestingModuleTests(String name) { super(name); } - -} diff --git a/testing/src/test/java/org/aspectj/testing/WeaveSpec.java b/testing/src/test/java/org/aspectj/testing/WeaveSpec.java new file mode 100644 index 000000000..2793b63ac --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/WeaveSpec.java @@ -0,0 +1,164 @@ +/* ******************************************************************* + * Copyright (c) 2005 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.aspectj.tools.ajc.AjcTestCase; +import org.aspectj.tools.ajc.CompilationResult; + +/** + * @author colyer + * + * TODO To change the template for this generated type comment go to + * Window - Preferences - Java - Code Style - Code Templates + */ +public class WeaveSpec extends CompileSpec { + + private String classesFiles; + private String aspectsFiles; + private List classFilesFromClasses; + + /* (non-Javadoc) + * @see org.aspectj.testing.ITestStep#execute(org.aspectj.tools.ajc.AjcTestCase) + */ + public void execute(AjcTestCase inTestCase) { + String failMessage = "test \"" + getTest().getTitle() + "\" failed"; + try { + File base = new File(getBaseDir()); + classFilesFromClasses = new ArrayList(); + setFiles(classesFiles); + String[] args = buildArgs(); + CompilationResult result = inTestCase.ajc(base,args); + inTestCase.assertNoMessages(result,failMessage); + File sandbox = inTestCase.getSandboxDirectory(); + createJar(sandbox,"classes.jar",true); + + inTestCase.setShouldEmptySandbox(false); + setFiles(aspectsFiles); + String options = getOptions(); + if (options == null) { + setOptions(""); + } + setClasspath("classes.jar"); + args = buildArgs(); + result = inTestCase.ajc(base,args); + inTestCase.assertNoMessages(result,failMessage); + createJar(sandbox,"aspects.jar",false); + + args = buildWeaveArgs(); + inTestCase.setShouldEmptySandbox(false); + result = inTestCase.ajc(base,args); + AjcTestCase.MessageSpec messageSpec = buildMessageSpec(); + inTestCase.assertMessages(result,failMessage,messageSpec); + inTestCase.setShouldEmptySandbox(false); // so subsequent steps in same test see my results + } catch (IOException e) { + AjcTestCase.fail(failMessage + " " + e); + } + } + + public void setClassesFiles(String files) { + this.classesFiles = files; + } + + public void setAspectsFiles(String files) { + this.aspectsFiles = files; + } + + /** + * Find all the .class files under the dir, package them into a jar file, + * and then delete them. + * @param inDir + * @param name + */ + private void createJar(File inDir, String name, boolean isClasses) throws IOException { + File outJar = new File(inDir,name); + FileOutputStream fos = new FileOutputStream(outJar); + JarOutputStream jarOut = new JarOutputStream(fos); + List classFiles = new ArrayList(); + List toExclude = isClasses ? Collections.emptyList() : classFilesFromClasses; + collectClassFiles(inDir,classFiles,toExclude); + if (isClasses) classFilesFromClasses = classFiles; + String prefix = inDir.getPath() + File.separator; + for (File f: classFiles) { + String thisPath = f.getPath(); + if (thisPath.startsWith(prefix)) { + thisPath = thisPath.substring(prefix.length()); + } + JarEntry entry = new JarEntry(thisPath); + jarOut.putNextEntry(entry); + copyFile(f,jarOut); + jarOut.closeEntry(); + } + jarOut.flush(); + jarOut.close(); + } + + private void collectClassFiles(File inDir, List inList, List toExclude) { + File[] contents = inDir.listFiles(); + for (int i = 0; i < contents.length; i++) { + if (contents[i].getName().endsWith(".class")) { + if (!toExclude.contains(contents[i])) { + inList.add(contents[i]); + } + } else if (contents[i].isDirectory()) { + collectClassFiles(contents[i],inList, toExclude); + } + } + } + + private void copyFile(File f, OutputStream dest) throws IOException { + FileInputStream fis = new FileInputStream(f); + byte[] buf = new byte[4096]; + int read = -1; + while((read = fis.read(buf)) != -1) { + dest.write(buf,0,read); + } + fis.close(); + } + + private String[] buildWeaveArgs() { + StringBuffer args = new StringBuffer(); + if (getOptions() != null) { + StringTokenizer strTok = new StringTokenizer(getOptions(),","); + while (strTok.hasMoreTokens()) { + args.append(strTok.nextToken()); + args.append(" "); + } + } + args.append("-inpath "); + args.append("classes.jar"); + args.append(File.pathSeparator); + args.append("aspects.jar"); + args.append(" "); + args.append("-aspectpath "); + args.append("aspects.jar"); + String argumentString = args.toString(); + StringTokenizer strTok = new StringTokenizer(argumentString," "); + String[] ret = new String[strTok.countTokens()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = strTok.nextToken(); + } + return ret; + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCase.java b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCase.java new file mode 100644 index 000000000..13bbf844e --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCase.java @@ -0,0 +1,522 @@ +/* ******************************************************************* + * Copyright (c) 2004 IBM Corporation + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Adrian Colyer, + * ******************************************************************/ +package org.aspectj.testing; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import org.apache.commons.digester.Digester; +import org.aspectj.apache.bcel.classfile.Attribute; +import org.aspectj.apache.bcel.classfile.JavaClass; +import org.aspectj.apache.bcel.classfile.LocalVariable; +import org.aspectj.apache.bcel.classfile.LocalVariableTable; +import org.aspectj.apache.bcel.classfile.Method; +import org.aspectj.apache.bcel.util.ClassPath; +import org.aspectj.apache.bcel.util.SyntheticRepository; +import org.aspectj.tools.ajc.AjcTestCase; +import org.aspectj.tools.ajc.CompilationResult; +import org.aspectj.util.FileUtil; +import org.aspectj.weaver.AjAttribute; +import org.aspectj.weaver.ResolvedMember; +import org.aspectj.weaver.ResolvedType; +import org.aspectj.weaver.WeaverStateInfo; +import org.aspectj.weaver.AjAttribute.WeaverState; +import org.aspectj.weaver.AjAttribute.WeaverVersionInfo; +import org.aspectj.weaver.bcel.BcelConstantPoolReader; + +import junit.extensions.TestSetup; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Root class for all Test suites that are based on an AspectJ XML test suite file. Extends AjcTestCase allowing a mix of + * programmatic and spec-file driven testing. See org.aspectj.systemtest.incremental.IncrementalTests for an example of this mixed + * style. + *

    + * The class org.aspectj.testing.MakeTestClass will generate a subclass of this class for you, given a suite spec. file as input... + *

    + */ +public abstract class XMLBasedAjcTestCase extends AjcTestCase { + + private static Map testMap = new HashMap(); + private static boolean suiteLoaded = false; + private AjcTest currentTest = null; + private Stack clearTestAfterRun = new Stack(); + + public XMLBasedAjcTestCase() { + } + + /** + * You must define a suite() method in subclasses, and return the result of calling this method. (Don't you hate static methods + * in programming models). For example: + * + *
    +	 * public static Test suite() {
    +	 * 	return XMLBasedAjcTestCase.loadSuite(MyTestCaseClass.class);
    +	 * }
    +	 * 
    + * + * @param testCaseClass + * @return + */ + public static Test loadSuite(Class testCaseClass) { + TestSuite suite = new TestSuite(testCaseClass.getName()); + suite.addTestSuite(testCaseClass); + TestSetup wrapper = new TestSetup(suite) { + /* + * (non-Javadoc) + * + * @see junit.extensions.TestSetup#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + suiteLoaded = false; + } + + /* + * (non-Javadoc) + * + * @see junit.extensions.TestSetup#tearDown() + */ + protected void tearDown() throws Exception { + super.tearDown(); + suiteLoaded = false; + } + }; + return wrapper; + } + + /** + * The file containing the XML specification for the tests. + */ + protected abstract File getSpecFile(); + + /* + * Return a map from (String) test title -> AjcTest + */ + protected Map getSuiteTests() { + return testMap; + } + + protected WeaverStateInfo getWeaverStateInfo(JavaClass jc) { + WeaverStateInfo wsi = null; + try { + for (Attribute attribute : jc.getAttributes()) { + if (attribute.getName().equals("org.aspectj.weaver.WeaverState")) { + if (wsi != null) { + fail("Found two WeaverState attributes"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + attribute.dump(new DataOutputStream(baos)); + baos.close(); + byte[] byteArray = baos.toByteArray(); + byte[] newbytes = new byte[byteArray.length-6]; + System.arraycopy(byteArray, 6, newbytes, 0, newbytes.length); + WeaverState read = (WeaverState) + AjAttribute.read(new WeaverVersionInfo(), WeaverState.AttributeName, + newbytes, null, null, + new BcelConstantPoolReader(jc.getConstantPool())); + wsi = read.reify(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return wsi; + } + + /** + * This helper method runs the test with the given title in the suite spec file. All tests steps in given ajc-test execute in + * the same sandbox. + */ + protected void runTest(String title, boolean print) { + try { + currentTest = (AjcTest) testMap.get(title); + final boolean clearTest = clearTestAfterRun(); + if (currentTest == null) { + if (clearTest) { + System.err.println("test already run: " + title); + return; + } else { + fail("No test '" + title + "' in suite."); + } + } + boolean run = currentTest.runTest(this); + assertTrue("Test not run", run); + if (clearTest) { + testMap.remove(title); + } + } finally { + if (print) { + System.out.println("SYSOUT"); + System.out.println(ajc.getLastCompilationResult().getStandardOutput()); + } + } + } + + protected void runTest(String title) { + runTest(title, false); + } + + /** + * Get the currently executing test. Useful for access to e.g. AjcTest.getTitle() etc.. + */ + protected AjcTest getCurrentTest() { + return currentTest; + } + + /** + * For use by the Digester. As the XML document is parsed, it creates instances of AjcTest objects, which are added to this + * TestCase by the Digester by calling this method. + */ + public void addTest(AjcTest test) { + testMap.put(test.getTitle(), test); + } + + protected final void pushClearTestAfterRun(boolean val) { + clearTestAfterRun.push(val ? Boolean.FALSE : Boolean.TRUE); + } + + protected final boolean popClearTestAfterRun() { + return clearTest(true); + } + + protected final boolean clearTestAfterRun() { + return clearTest(false); + } + + private boolean clearTest(boolean pop) { + if (clearTestAfterRun.isEmpty()) { + return false; + } + boolean result = clearTestAfterRun.peek().booleanValue(); + if (pop) { + clearTestAfterRun.pop(); + } + return result; + } + + /* + * The rules for parsing a suite spec file. The Digester using bean properties to match attributes in the XML document to + * properties in the associated classes, so this simple implementation should be very easy to maintain and extend should you + * ever need to. + */ + protected Digester getDigester() { + Digester digester = new Digester(); + digester.push(this); + digester.addObjectCreate("suite/ajc-test", AjcTest.class); + digester.addSetProperties("suite/ajc-test"); + digester.addSetNext("suite/ajc-test", "addTest", "org.aspectj.testing.AjcTest"); + digester.addObjectCreate("suite/ajc-test/compile", CompileSpec.class); + digester.addSetProperties("suite/ajc-test/compile"); + digester.addSetNext("suite/ajc-test/compile", "addTestStep", "org.aspectj.testing.ITestStep"); + digester.addObjectCreate("suite/ajc-test/file", FileSpec.class); + digester.addSetProperties("suite/ajc-test/file"); + digester.addSetNext("suite/ajc-test/file", "addTestStep", "org.aspectj.testing.ITestStep"); + digester.addObjectCreate("suite/ajc-test/run", RunSpec.class); + digester.addSetProperties("suite/ajc-test/run", "class", "classToRun"); + digester.addSetProperties("suite/ajc-test/run", "module", "moduleToRun"); + digester.addSetProperties("suite/ajc-test/run", "ltw", "ltwFile"); + digester.addSetProperties("suite/ajc-test/run", "xlintfile", "xlintFile"); + digester.addSetProperties("suite/ajc-test/run/stderr", "ordered", "orderedStderr"); + digester.addSetNext("suite/ajc-test/run", "addTestStep", "org.aspectj.testing.ITestStep"); + digester.addObjectCreate("*/message", ExpectedMessageSpec.class); + digester.addSetProperties("*/message"); + digester.addSetNext("*/message", "addExpectedMessage", "org.aspectj.testing.ExpectedMessageSpec"); + digester.addObjectCreate("suite/ajc-test/weave", WeaveSpec.class); + digester.addSetProperties("suite/ajc-test/weave"); + digester.addSetNext("suite/ajc-test/weave", "addTestStep", "org.aspectj.testing.ITestStep"); + + digester.addObjectCreate("suite/ajc-test/ant", AntSpec.class); + digester.addSetProperties("suite/ajc-test/ant"); + digester.addSetNext("suite/ajc-test/ant", "addTestStep", "org.aspectj.testing.ITestStep"); + digester.addObjectCreate("suite/ajc-test/ant/stderr", OutputSpec.class); + digester.addSetProperties("suite/ajc-test/ant/stderr"); + digester.addSetNext("suite/ajc-test/ant/stderr", "addStdErrSpec", "org.aspectj.testing.OutputSpec"); + digester.addObjectCreate("suite/ajc-test/ant/stdout", OutputSpec.class); + digester.addSetProperties("suite/ajc-test/ant/stdout"); + digester.addSetNext("suite/ajc-test/ant/stdout", "addStdOutSpec", "org.aspectj.testing.OutputSpec"); + + digester.addObjectCreate("suite/ajc-test/run/stderr", OutputSpec.class); + digester.addSetProperties("suite/ajc-test/run/stderr"); + digester.addSetNext("suite/ajc-test/run/stderr", "addStdErrSpec", "org.aspectj.testing.OutputSpec"); + digester.addObjectCreate("suite/ajc-test/run/stdout", OutputSpec.class); + digester.addSetProperties("suite/ajc-test/run/stdout"); + digester.addSetNext("suite/ajc-test/run/stdout", "addStdOutSpec", "org.aspectj.testing.OutputSpec"); + digester.addObjectCreate("*/line", OutputLine.class); + digester.addSetProperties("*/line"); + digester.addSetNext("*/line", "addLine", "org.aspectj.testing.OutputLine"); + return digester; + } + + /* + * (non-Javadoc) + * + * @see org.aspectj.tools.ajc.AjcTestCase#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + if (!suiteLoaded) { + testMap = new HashMap(); + System.out.println("LOADING SUITE: " + getSpecFile().getPath()); + Digester d = getDigester(); + try { + InputStreamReader isr = new InputStreamReader(new FileInputStream(getSpecFile())); + d.parse(isr); + } catch (Exception ex) { + fail("Unable to load suite " + getSpecFile().getPath() + " : " + ex); + } + suiteLoaded = true; + } + } + + protected long nextIncrement(boolean doWait) { + long time = System.currentTimeMillis(); + if (doWait) { + try { + Thread.sleep(1000); + } catch (InterruptedException intEx) { + } + } + return time; + } + + protected void copyFile(String from, String to) throws Exception { + String dir = getCurrentTest().getDir(); + FileUtil.copyFile(new File(dir + File.separator + from), new File(ajc.getSandboxDirectory(), to)); + } + + protected void copyFileAndDoIncrementalBuild(String from, String to) throws Exception { + copyFile(from, to); + CompilationResult result = ajc.doIncrementalCompile(); + assertNoMessages(result, "Expected clean compile from test '" + getCurrentTest().getTitle() + "'"); + } + + protected void copyFileAndDoIncrementalBuild(String from, String to, MessageSpec expectedResults) throws Exception { + String dir = getCurrentTest().getDir(); + FileUtil.copyFile(new File(dir + File.separator + from), new File(ajc.getSandboxDirectory(), to)); + CompilationResult result = ajc.doIncrementalCompile(); + assertMessages(result, "Test '" + getCurrentTest().getTitle() + "' did not produce expected messages", expectedResults); + } + + protected void deleteFile(String file) { + new File(ajc.getSandboxDirectory(), file).delete(); + } + + protected void deleteFileAndDoIncrementalBuild(String file, MessageSpec expectedResult) throws Exception { + deleteFile(file); + CompilationResult result = ajc.doIncrementalCompile(); + assertMessages(result, "Test '" + getCurrentTest().getTitle() + "' did not produce expected messages", expectedResult); + } + + protected void deleteFileAndDoIncrementalBuild(String file) throws Exception { + deleteFileAndDoIncrementalBuild(file, MessageSpec.EMPTY_MESSAGE_SET); + } + + protected void assertAdded(String file) { + assertTrue("File " + file + " should have been added", new File(ajc.getSandboxDirectory(), file).exists()); + } + + protected void assertDeleted(String file) { + assertFalse("File " + file + " should have been deleted", new File(ajc.getSandboxDirectory(), file).exists()); + } + + protected void assertUpdated(String file, long sinceTime) { + File f = new File(ajc.getSandboxDirectory(), file); + assertTrue("File " + file + " should have been updated", f.lastModified() > sinceTime); + } + + public SyntheticRepository createRepos(File cpentry) { + ClassPath cp = new ClassPath(cpentry + File.pathSeparator + System.getProperty("java.class.path")); + return SyntheticRepository.getInstance(cp); + } + + protected byte[] loadFileAsByteArray(File f) { + try { + byte[] bs = new byte[100000]; + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f)); + int pos = 0; + int len = 0; + while ((len=bis.read(bs, pos, 100000-pos))!=-1) { + pos+=len; + } + bis.close(); + return bs; + } catch (Exception e) { + return null; + } + } + + public JavaClass getClassFrom(File where, String clazzname) throws ClassNotFoundException { + SyntheticRepository repos = createRepos(where); + return repos.loadClass(clazzname); + } + + protected Method getMethodStartsWith(JavaClass jc, String prefix) { + return getMethodStartsWith(jc,prefix,1); + } + + protected Attribute getAttributeStartsWith(Attribute[] attributes, String prefix) { + StringBuilder buf = new StringBuilder(); + for (Attribute a: attributes) { + if (a.getName().startsWith(prefix)) { + return a; + } + buf.append(a.toString()).append("\n"); + } + fail("Failed to find '"+prefix+"' in attributes:\n"+buf.toString()); + return null; + } + + protected Method getMethodStartsWith(JavaClass jc, String prefix, int whichone) { + Method[] meths = jc.getMethods(); + for (int i = 0; i < meths.length; i++) { + Method method = meths[i]; + System.out.println(method); + if (method.getName().startsWith(prefix)) { + whichone--; + if (whichone==0) { + return method; + } + } + } + return null; + } + + /** + * Sort it by name then start position + */ + public List sortedLocalVariables(LocalVariableTable lvt) { + List l = new ArrayList(); + LocalVariable lv[] = lvt.getLocalVariableTable(); + for (int i = 0; i < lv.length; i++) { + LocalVariable lvEntry = lv[i]; + l.add(lvEntry); + } + Collections.sort(l, new MyComparator()); + return l; + } + + public String stringify(LocalVariableTable lvt, int slotIndex) { + LocalVariable lv[] = lvt.getLocalVariableTable(); + LocalVariable lvEntry = lv[slotIndex]; + StringBuffer sb = new StringBuffer(); + sb.append(lvEntry.getSignature()).append(" ").append(lvEntry.getName()).append("(").append(lvEntry.getIndex()) + .append(") start=").append(lvEntry.getStartPC()).append(" len=").append(lvEntry.getLength()); + return sb.toString(); + } + + public String stringify(List l, int slotIndex) { + LocalVariable lvEntry = (LocalVariable) l.get(slotIndex); + StringBuffer sb = new StringBuffer(); + sb.append(lvEntry.getSignature()).append(" ").append(lvEntry.getName()).append("(").append(lvEntry.getIndex()) + .append(") start=").append(lvEntry.getStartPC()).append(" len=").append(lvEntry.getLength()); + return sb.toString(); + } + + public String stringify(LocalVariableTable lvt) { + if (lvt == null) { + return ""; + } + StringBuffer sb = new StringBuffer(); + sb.append("LocalVariableTable. Entries=#" + lvt.getTableLength()).append("\n"); + LocalVariable lv[] = lvt.getLocalVariableTable(); + for (int i = 0; i < lv.length; i++) { + LocalVariable lvEntry = lv[i]; + sb.append(lvEntry.getSignature()).append(" ").append(lvEntry.getName()).append("(").append(lvEntry.getIndex()) + .append(") start=").append(lvEntry.getStartPC()).append(" len=").append(lvEntry.getLength()).append("\n"); + } + + return sb.toString(); + } + + public static class CountingFilenameFilter implements FilenameFilter { + + private String suffix; + private int count; + + public CountingFilenameFilter(String s) { + this.suffix = s; + } + + public boolean accept(File dir, String name) { + if (name.endsWith(suffix)) { + count++; + } + return false; + } + + public int getCount() { + return count; + } + } + + public static class MyComparator implements Comparator { + public int compare(LocalVariable o1, LocalVariable o2) { + LocalVariable l1 = (LocalVariable) o1; + LocalVariable l2 = (LocalVariable) o2; + if (l1.getName().equals(l2.getName())) { + return l1.getStartPC() - l2.getStartPC(); + } else { + return l1.getName().compareTo(l2.getName()); + } + } + + } + + protected Method getMethodFromClass(JavaClass clazz, String methodName) { + Method[] meths = clazz.getMethods(); + for (int i = 0; i < meths.length; i++) { + Method method = meths[i]; + if (method.getName().equals(methodName)) { + return meths[i]; + } + } + return null; + } + + protected File getClassResource(String resourceName) { + return new File(getClass().getResource(resourceName).getFile()); + } + + protected Method findMethod(JavaClass jc, String string) { + for (Method m : jc.getMethods()) { + if (m.getName().equals(string)) { + return m; + } + } + return null; + } + + protected ResolvedMember findMethod(ResolvedType outerType, String string) { + for (ResolvedMember method: outerType.getDeclaredMethods()) { + if (method.getName().equals(string)) { + return method; + } + } + return null; + } + + +} diff --git a/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava10OrLater.java b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava10OrLater.java new file mode 100644 index 000000000..4a8c63f11 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava10OrLater.java @@ -0,0 +1,31 @@ +/* ******************************************************************* + * Copyright (c) 2018 Contributors + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Andy Clement + * ******************************************************************/ +package org.aspectj.testing; + +import org.aspectj.util.LangUtil; + +/** + * Ensure sure tests are running on the right level of JDK. + * + * @author Andy Clement + */ +public abstract class XMLBasedAjcTestCaseForJava10OrLater extends XMLBasedAjcTestCase { + + @Override + public void runTest(String title) { + if (!LangUtil.is10VMOrGreater()) { + throw new IllegalStateException("These tests should be run on Java 10 or later"); + } + super.runTest(title); + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava11OrLater.java b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava11OrLater.java new file mode 100644 index 000000000..b71fc19e9 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava11OrLater.java @@ -0,0 +1,31 @@ +/* ******************************************************************* + * Copyright (c) 2018 Contributors + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Andy Clement + * ******************************************************************/ +package org.aspectj.testing; + +/** + * Makes sure tests are running on the right level of JDK. + * + * @author Andy Clement + */ +public abstract class XMLBasedAjcTestCaseForJava11OrLater extends XMLBasedAjcTestCase { + + @Override + public void runTest(String title) { + // Check we are on Java11 + String property = System.getProperty("java.version"); + if (!property.startsWith("11")) { + throw new IllegalStateException("These tests should be run on Java 11 or later"); + } + super.runTest(title); + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava9OrLater.java b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava9OrLater.java new file mode 100644 index 000000000..205b55273 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/XMLBasedAjcTestCaseForJava9OrLater.java @@ -0,0 +1,32 @@ +/* ******************************************************************* + * Copyright (c) 2018 Contributors + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Andy Clement + * ******************************************************************/ +package org.aspectj.testing; + +import org.aspectj.util.LangUtil; + +/** + * Makes sure tests are running on the right level of JDK. + * + * @author Andy Clement + */ +public abstract class XMLBasedAjcTestCaseForJava9OrLater extends XMLBasedAjcTestCase { + + @Override + public void runTest(String title) { + // Check we are on Java9 or later + if (!LangUtil.is19VMOrGreater()) { + throw new IllegalStateException("These tests should be run on Java 9 or later"); + } + super.runTest(title); + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/ajde/CompileCommand.java b/testing/src/test/java/org/aspectj/testing/ajde/CompileCommand.java new file mode 100644 index 000000000..86c5fbaf7 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/ajde/CompileCommand.java @@ -0,0 +1,439 @@ +/* ******************************************************************* + * Copyright (c) 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Wes Isberg initial implementation + * Helen Hawkins Converted to new interface (bug 148190) + * ******************************************************************/ + +package org.aspectj.testing.ajde; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.aspectj.ajde.core.AjCompiler; +import org.aspectj.ajde.core.IBuildMessageHandler; +import org.aspectj.ajde.core.IBuildProgressMonitor; +import org.aspectj.ajde.core.ICompilerConfiguration; +import org.aspectj.ajde.core.IOutputLocationManager; +import org.aspectj.ajde.core.JavaOptions; +import org.aspectj.bridge.AbortException; +import org.aspectj.bridge.ICommand; +import org.aspectj.bridge.IMessage; +import org.aspectj.bridge.IMessage.Kind; +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.bridge.MessageHandler; +import org.aspectj.testing.harness.bridge.Globals; +import org.aspectj.util.FileUtil; + +/** + * This re-uses the same config file to setup ajde so that recompiles appear to be of the same configuration. + * + * @since Java 1.3 (uses dynamic proxies) + */ +public class CompileCommand implements ICommand { + // time out waiting for build at three minutes + long MAX_TIME = 180 * 1000; + // this proxy ignores calls + InvocationHandler proxy = new VoidInvocationHandler(); + InvocationHandler loggingProxy = new LoggingInvocationHandler(); + MyMessageHandler myHandler = new MyMessageHandler(); + long endTime; + boolean buildNextFresh; + File tempDir; + + private AjCompiler compiler; + + /** + * Clients call this before repeatCommand as a one-shot request for a full rebuild of the same configuration. (Requires a + * downcast from ICommand to CompileCommand.) + */ + public void buildNextFresh() { + buildNextFresh = true; + } + + // --------- ICommand interface + public boolean runCommand(String[] args, IMessageHandler handler) { + setup(args); + myHandler.start(); + long startTime = System.currentTimeMillis(); + try { + compiler.buildFresh(); + } finally { + runCommandCleanup(); + } + return !myHandler.hasError(); + } + + public boolean repeatCommand(IMessageHandler handler) { + myHandler.start(); + long startTime = System.currentTimeMillis(); + // System.err.println("recompiling..."); + if (buildNextFresh) { + buildNextFresh = false; + compiler.buildFresh(); + } else { + compiler.build(); + } + return !myHandler.hasError(); + } + + void runCommandCleanup() { + if (null != tempDir) { + FileUtil.deleteContents(tempDir); + tempDir.delete(); + } + } + + // set by build progress monitor when done + void setEndTime(long endTime) { + this.endTime = endTime; + } + + private void setup(String[] args) { + File config = writeConfig(args); + if (null == config) { + throw new Error("unable to write config file"); + } + IBuildProgressMonitor buildProgressMonitor = new MyBuildProgressMonitor(); + String classesDir = "../testing/bin/classes"; + for (int i = 0; i < args.length; i++) { + if ("-d".equals(args[i]) && ((1 + i) < args.length)) { + classesDir = args[1 + i]; + break; + } + } + MyCompilerConfig compilerConfig = new MyCompilerConfig(); + compiler = new AjCompiler("blah", compilerConfig, buildProgressMonitor, myHandler); + } + + private File writeConfig(String[] args) { + tempDir = FileUtil.getTempDir("CompileCommand"); + File result = new File(tempDir, "config.lst"); + OutputStream out = null; + try { + out = new FileOutputStream(result); + PrintStream outs = new PrintStream(out, true); + for (int i = 0; i < args.length; i++) { + outs.println(args[i]); + } + return result; + } catch (IOException e) { + return null; + } finally { + try { + out.close(); + } catch (IOException e) { + } + } + } + + // private Object makeLoggingProxy(Class interfac) { + // return Proxy.newProxyInstance( + // interfac.getClassLoader(), + // new Class[] { interfac }, + // loggingProxy); + // } + + private Object makeProxy(Class interfac) { + return Proxy.newProxyInstance(interfac.getClassLoader(), new Class[] { interfac }, proxy); + } +} + +class MyMessageHandler implements IBuildMessageHandler { + + boolean hasError; + boolean hasWarning; + + private MessageHandler messageHandler = new MessageHandler(false); + + public boolean handleMessage(IMessage message) throws AbortException { + maintainHasWarning(message.getKind()); + return messageHandler.handleMessage(message); + } + + private void maintainHasWarning(IMessage.Kind kind) { + if (!hasError) { + if (IMessage.ERROR.isSameOrLessThan(kind)) { + hasError = true; + hasWarning = true; + } + } + if (!hasWarning && IMessage.WARNING.isSameOrLessThan(kind)) { + hasWarning = true; + } + } + + public boolean hasWarning() { + return hasWarning; + } + + public boolean hasError() { + return hasError; + } + + public void start() { + hasWarning = false; + hasError = false; + messageHandler.init(true); + } + + public void dontIgnore(Kind kind) { + messageHandler.dontIgnore(kind); + } + + public void ignore(Kind kind) { + messageHandler.ignore(kind); + } + + public boolean isIgnoring(Kind kind) { + return messageHandler.isIgnoring(kind); + } + +} + +class MyBuildProgressMonitor implements IBuildProgressMonitor { + + public void begin() { + } + + public void finish(boolean wasFullBuild) { + } + + public boolean isCancelRequested() { + return false; + } + + public void setProgress(double percentDone) { + } + + public void setProgressText(String text) { + } + +} + +class VoidInvocationHandler implements InvocationHandler { + public Object invoke(Object me, Method method, Object[] args) throws Throwable { + // System.err.println("Proxying" + // // don't call toString on self b/c proxied + // // + " me=" + me.getClass().getName() + // + " method=" + method + // + " args=" + (LangUtil.isEmpty(args) + // ? "[]" : Arrays.asList(args).toString())); + return null; + } +} + +class LoggingInvocationHandler implements InvocationHandler { + public Object invoke(Object me, Method method, Object[] args) throws Throwable { + System.err.println("Proxying " + render(method, args)); + return null; + } + + public static String render(Class c) { + if (null == c) { + return "(Class) null"; + } + String result = c.getName(); + if (result.startsWith("java")) { + int loc = result.lastIndexOf("."); + if (-1 != loc) { + result = result.substring(loc + 1); + } + } + return result; + } + + public static String render(Method method, Object[] args) { + StringBuffer sb = new StringBuffer(); + sb.append(render(method.getReturnType())); + sb.append(" "); + sb.append(method.getName()); + sb.append("("); + Class[] parmTypes = method.getParameterTypes(); + int parmTypesLength = (null == parmTypes ? 0 : parmTypes.length); + int argsLength = (null == args ? 0 : args.length); + boolean doType = (parmTypesLength == argsLength); + for (int i = 0; i < argsLength; i++) { + if (i > 0) { + sb.append(", "); + } + if (doType) { + sb.append("("); + sb.append(render(parmTypes[i])); + sb.append(") "); + } + if (null == args[i]) { + sb.append("null"); + } else { // also don't recurse into proxied toString? + sb.append(args[i].toString()); + } + } + sb.append(")"); + return sb.toString(); + } +} + +class MyCompilerConfig implements ICompilerConfiguration { + + private Set inpath; + private Set aspectPath; + private String outJar; + private IOutputLocationManager locationMgr; + + public Set getAspectPath() { + return aspectPath; + } + + public void setAspectPath(Set path) { + aspectPath = path; + } + + public String getClasspath() { + return Globals.S_aspectjrt_jar; + } + + public Set getInpath() { + return inpath; + } + + public void setInpath(Set input) { + inpath = input; + } + + public Map getJavaOptionsMap() { + return JavaOptions.getDefaultJavaOptions(); + } + + public List getProjectXmlConfigFiles() { + return Collections.EMPTY_LIST; + } + + public String getOutJar() { + return outJar; + } + + public void configurationRead() { + } + + public void setOutJar(String input) { + outJar = input; + } + + public IOutputLocationManager getOutputLocationManager() { + if (locationMgr == null) { + locationMgr = new MyOutputLocationManager(); + } + return locationMgr; + } + + public String getNonStandardOptions() { + return null; + } + + public List getProjectSourceFiles() { + return null; + } + + public List getProjectSourceFilesChanged() { + return null; + } + + public Map getSourcePathResources() { + return null; + } + + public int getConfigurationChanges() { + return ICompilerConfiguration.EVERYTHING; + } + + public List getClasspathElementsWithModifiedContents() { + return null; + } + + public String getProjectEncoding() { + return null; + } + + public String getProcessor() { + return null; + } + + public String getProcessorPath() { + return null; + } + + @Override + public String getModulepath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getModuleSourcepath() { + // TODO Auto-generated method stub + return null; + } + +} + +class MyOutputLocationManager implements IOutputLocationManager { + + public List getAllOutputLocations() { + return null; + } + + public File getDefaultOutputLocation() { + return null; + } + + public File getOutputLocationForClass(File compilationUnit) { + return null; + } + + public File getOutputLocationForResource(File resource) { + return null; + } + + public String getUniqueIdentifier() { + return null; + } + + public Map getInpathMap() { + return Collections.EMPTY_MAP; + } + + public String getSourceFolderForFile(File sourceFile) { + return null; + } + + public void reportFileWrite(String outputfile, int filetype) { + } + + public void reportFileRemove(String outputfile, int filetype) { + } + + public int discoverChangesSince(File dir, long buildtime) { + return 0; + } + + public String getProjectEncoding() { + return null; + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/AbstractRunSpec.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/AbstractRunSpec.java new file mode 100644 index 000000000..4e61a512c --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/AbstractRunSpec.java @@ -0,0 +1,918 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * Wes Isberg 2004 updates + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +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; + +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.testing.run.IRunIterator; +import org.aspectj.testing.util.BridgeUtil; +import org.aspectj.testing.util.options.Option; +import org.aspectj.testing.util.options.Option.InvalidInputException; +import org.aspectj.testing.util.options.Options; +import org.aspectj.testing.util.options.Values; +import org.aspectj.testing.xml.IXmlWritable; +import org.aspectj.testing.xml.SoftMessage; +import org.aspectj.testing.xml.XMLWriter; +import org.aspectj.util.LangUtil; + +/** + * Base class for initialization of components expecting messages, options, files/paths, and source locations (resolved files), and + * potentially containing child Spec. + *

    + * initialization: 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. + *

    + * subclassing: 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. + *

    + * bean properties: 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). + *

    + * Coordination with writers: 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 { + + /** true if we expect to use a staging directory */ + boolean isStaging; + + /** true if this spec permits bad input (e.g., to test error handling) */ + boolean badInput; + + protected String description; + + /** optional source location of the specification itself */ + protected ISourceLocation sourceLocation; + + private BitSet skipSet; + private boolean skipAll; + + protected String xmlElementName; // nonfinal only for clone() + protected final ArrayList keywords; + protected final IMessageHolder /* IMessage */messages; + protected final ArrayList options; + protected final ArrayList paths; + // XXXXXunused protected final ArrayList /*ISourceLocation*/ sourceLocations; // XXX remove? + protected final ArrayList children; + protected final ArrayList 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 boolean skipIfAnyChildSkipped; // nonfinal only for cloning + + public AbstractRunSpec(String xmlElementName) { + this(xmlElementName, true); + } + + public AbstractRunSpec(String xmlElementName, boolean skipIfAnyChildSkipped) { + if (null == xmlElementName) { + xmlElementName = "spec"; + } + this.xmlElementName = xmlElementName; + messages = new MessageHandler(true); + options = new ArrayList(); + paths = new ArrayList(); + // XXXXXunused 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; + } + + public void setBadInput(boolean badInput) { + this.badInput = badInput; + } + + 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); + } + + /** @return ((null == s) || (0 == s.trim().length())); */ + public static boolean isEmptyTrimmed(String s) { + return ((null == s) || (0 == s.length()) || (0 == s.trim().length())); + } + + /** Add keyword if non-empty and not duplicate */ + public void addKeyword(String keyword) { + if (!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]); + } + } + } + + // --------------- (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]); + } + + 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]); + } + } + } + + // --------------------- 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) { + if (!messages.handleMessage(message)) { + String s = "invalid message: " + message; + throw new IllegalArgumentException(s); + } + } + } + + public void addMessage(String message) { + if (null != message) { + addMessage(BridgeUtil.readMessage(message)); + } + } + + /** + * 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 { + String m = "not message: " + o; + addMessage(new Message(m, IMessage.WARNING, null, null)); + } + } + } + } + + /** @return int number of message of this kind (optionally or greater */ + public int numMessages(IMessage.Kind kind, boolean orGreater) { + return messages.numMessages(kind, orGreater); + } + + public IMessageHolder getMessages() { + return messages; + } + + 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++) { + IRunSpec 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) { + AbstractRunSpec arsChild = (AbstractRunSpec) child; + if (!arsChild.adoptParentValues(runtime, handler)) { + skipSet.set(i); + if (!skipped) { + skipped = true; + if (skipIfAnyChildSkipped) { // no need to continue checking + skipAll = true; + 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 XMLWriter.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 = XMLWriter.flattenList(list); + // return XMLWriter.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, XMLWriter.flattenList(keywords)); + } + if (!LangUtil.isEmpty(xmlNames.optionsName) && !LangUtil.isEmpty(options)) { + out.printAttribute(xmlNames.optionsName, XMLWriter.flattenList(options)); + } + if (!LangUtil.isEmpty(xmlNames.pathsName) && !LangUtil.isEmpty(paths)) { + out.printAttribute(xmlNames.pathsName, XMLWriter.flattenList(paths)); + } + if (!LangUtil.isEmpty(xmlNames.commentName) && !LangUtil.isEmpty(comment)) { + out.printAttribute(xmlNames.commentName, comment); + } + if (isStaging && !LangUtil.isEmpty(xmlNames.stagingName)) { + out.printAttribute(xmlNames.stagingName, "true"); + } + if (badInput && !LangUtil.isEmpty(xmlNames.badInputName)) { + out.printAttribute(xmlNames.badInputName, "true"); + } + } + + /** + * 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 < messages.numMessages(null, true)) { + SoftMessage.writeXml(out, messages); + } + + } + + /** + * 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() { + StringBuffer result = new StringBuffer(); + addListCount("options", options, result); + addListCount("paths", paths, result); + // XXXXXunused addListCount("sourceLocations", sourceLocations, result); + List messagesList = messages.getUnmodifiableListView(); + addListCount("messages", messagesList, result); + + return result.toString().trim(); + } + + public String toLongString() { + String mssg = ""; + if (0 < messages.numMessages(null, true)) { + mssg = " expected messages (" + MessageUtil.renderCounts(messages) + ")"; + } + return getPrintName() + containedToLongString() + mssg.trim(); + } + + /** @return String of the form (# [options|paths|locations|messages]).. */ + protected String containedToLongString() { + StringBuffer result = new StringBuffer(); + addListEntries("options", options, result); + addListEntries("paths", paths, result); + // XXXXXunused addListEntries("sourceLocations", sourceLocations, result); + List messagesList = messages.getUnmodifiableListView(); + addListEntries("messages", messagesList, result); + + return result.toString(); + } + + protected void initClone(AbstractRunSpec spec) throws CloneNotSupportedException { + /* + * clone associated objects only if not (used as?) read-only. + */ + spec.badInput = badInput; + spec.children.clear(); + for (Iterator iter = children.iterator(); iter.hasNext();) { + // clone these... + IRunSpec child = iter.next(); + // require all child classes to support clone? + if (child instanceof AbstractRunSpec) { + spec.addChild((AbstractRunSpec) ((AbstractRunSpec) child).clone()); + } else { + throw new Error("unable to clone " + child); + } + } + spec.comment = comment; + spec.description = description; + spec.dirChanges.clear(); + spec.dirChanges.addAll(dirChanges); + spec.isStaging = spec.isStaging; + spec.keywords.clear(); + spec.keywords.addAll(keywords); + spec.messages.clearMessages(); + MessageUtil.handleAll(spec.messages, messages, false); + spec.options.clear(); + spec.options.addAll(options); + spec.paths.clear(); + spec.paths.addAll(paths); + spec.runtime.copy(runtime); + spec.skipAll = skipAll; + spec.skipIfAnyChildSkipped = skipIfAnyChildSkipped; + if (null != skipSet) { + spec.skipSet = new BitSet(); + spec.skipSet.or(skipSet); + } + // spec.sourceLocation = sourceLocation; + // spec.sourceLocations.clear(); + // XXXXXunused spec.sourceLocations.addAll(sourceLocations); + spec.xmlElementName = xmlElementName; + spec.xmlNames = ((AbstractRunSpec.XMLNames) xmlNames.clone()); + } + + private static void addListCount(String name, List list, StringBuffer sink) { + int size = list.size(); + if ((null != list) && (0 < size)) { + sink.append(" " + size + " "); + sink.append(name); + } + } + + private static void addListEntries(String name, List list, StringBuffer sink) { + if ((null != list) && (0 < list.size())) { + sink.append(" " + list.size() + " "); + sink.append(name); + sink.append(": "); + sink.append(list.toString()); + } + } + + 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. XXX this really should be replaced with nested + * properties associated logical name with actual name (or placeholders for "unused" and "default"). + */ + public static class XMLNames { + public static final XMLNames DEFAULT = new XMLNames(null, "description", "sourceLocation", "keywords", "options", "paths", + "comment", "staging", "badInput", false, false, false); + final String descriptionName; + final String sourceLocationName; + final String keywordsName; + final String optionsName; + final String pathsName; + final String commentName; + final String stagingName; + final String badInputName; + final boolean skipDirChanges; + final boolean skipMessages; + final boolean skipChildren; + + protected Object clone() { + return new XMLNames(null, descriptionName, sourceLocationName, keywordsName, optionsName, pathsName, commentName, + stagingName, badInputName, skipDirChanges, skipMessages, skipChildren); + } + + // not runtime, skipAll, skipIfAnyChildSkipped, skipSet + // sourceLocations + /** + * 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, String stagingName, String badInputName, 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); + this.stagingName = (null != stagingName ? stagingName : defaultNames.stagingName); + this.badInputName = (null != badInputName ? badInputName : defaultNames.badInputName); + } else { + this.descriptionName = descriptionName; + this.sourceLocationName = sourceLocationName; + this.keywordsName = keywordsName; + this.optionsName = optionsName; + this.pathsName = pathsName; + this.commentName = commentName; + this.stagingName = stagingName; + this.badInputName = badInputName; + } + } + } + + /** subclasses implement this to create and set up a run */ + abstract public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator); + + /** 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 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 accepted by validOptions, optionally removing the parent option. + * + * @param validOptions String[] of options to extract + * @param remove if true, then remove any parent option matched + * @return String[] containing any validOptions[i] in parentOptions + * + */ + public Values extractOptions(Options validOptions, boolean remove, StringBuffer errors) { + Values result = Values.EMPTY; + if (null == errors) { + errors = new StringBuffer(); + } + if (null == validOptions) { + errors.append("null options"); + return result; + } + if (LangUtil.isEmpty(parentOptions)) { + return result; + } + // boolean haveOption = false; + String[] parents = (String[]) parentOptions.toArray(new String[0]); + try { + result = validOptions.acceptInput(parents); + } catch (InvalidInputException e) { + errors.append(e.getFullMessage()); + return result; + } + if (remove) { + Option.Value[] values = result.asArray(); + for (int i = 0; i < values.length; i++) { + Option.Value value = values[i]; + if (null == value) { + continue; + } + final int max = i + value.option.numArguments(); + if (max > i) { + if (max >= parents.length) { + errors.append("expecting more args for " + value.option + " at [" + i + "]: " + Arrays.asList(parents)); + return result; + } + // XXX verify + for (int j = i; j < max; j++) { + parentOptions.remove(parents[j]); + } + i = max - 1; + } + } + } + return result; + } + + /** + * 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 = 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/test/java/org/aspectj/testing/harness/bridge/AjcMessageHandler.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/AjcMessageHandler.java new file mode 100644 index 000000000..f4a88c3b2 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/AjcMessageHandler.java @@ -0,0 +1,343 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +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.bridge.IMessage.Kind; +import org.aspectj.testing.util.BridgeUtil; +import org.aspectj.testing.util.Diffs; +import org.aspectj.util.LangUtil; + +/** + * Handle messages during test and calculate differences + * between expected and actual messages. + */ +public class AjcMessageHandler extends MessageHandler { + + /** Comparator for enclosed IMessage diffs */ + public static final Comparator COMP_IMessage = + BridgeUtil.Comparators.MEDIUM_IMessage; + + /** Comparator for enclosed File diffs */ + public static final Comparator COMP_File = BridgeUtil.Comparators.WEAK_File; + + /** unmodifiable list of IMessage messages of any type */ + private final List expectedMessagesAsList; + + /** IMessageHolder variant of expectedMessagesAsList */ + private final IMessageHolder expectedMessages; + + /** number of messages FAIL or worse */ + private final int numExpectedFailed; + + /** true if there were no error or worse messages expected */ + private final boolean expectingCommandTrue; + + /** unmodifiable list of File expected to be recompiled */ + private final List expectedRecompiled; + // Unused now, but reinstate when supported + + /** if true, ignore warnings when calculating diffs and passed() */ + private final boolean ignoreWarnings; + + /** list of File actually recompiled */ + private List actualRecompiled; + + /** cache expected/actual diffs, nullify if any new message */ + private transient CompilerDiffs diffs; + + AjcMessageHandler(IMessageHolder expectedMessages) { + this(expectedMessages, false); + } + /** + * @param messages the (constant) IMessageHolder with expected messages + */ + AjcMessageHandler( + IMessageHolder expectedMessages, + boolean ignoreWarnings) { + LangUtil.throwIaxIfNull(messages, "messages"); + this.expectedMessages = expectedMessages; + expectedMessagesAsList = expectedMessages.getUnmodifiableListView(); + expectedRecompiled = Collections.EMPTY_LIST; + this.ignoreWarnings = ignoreWarnings; + int fails = 0; + int errors = 0; + for (Iterator iter = expectedMessagesAsList.iterator(); + iter.hasNext(); + ) { + IMessage m = (IMessage) iter.next(); + IMessage.Kind kind = m.getKind(); + if (IMessage.FAIL.isSameOrLessThan(kind)) { + fails++; + } else if (m.isError()) { + errors++; + } + } + expectingCommandTrue = (0 == (errors + fails)); + numExpectedFailed = fails; + } + + /** 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"); + } + super.handleMessage(message); + return expecting(message); + } + + /** + * 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); + } + + /** Generate differences between expected and actual errors and warnings */ + public CompilerDiffs getCompilerDiffs() { + if (null == diffs) { + final List expected; + final List actual; + if (!ignoreWarnings) { + expected = expectedMessages.getUnmodifiableListView(); + actual = this.getUnmodifiableListView(); + } else { + expected = + Arrays.asList( + expectedMessages.getMessages(IMessage.ERROR, true)); + actual = Arrays.asList(this.getMessages(IMessage.ERROR, true)); + } + // we ignore unexpected info messages, + // but we do test for expected ones + final Diffs messages; + boolean usingNew = true; // XXX extract old API's after shake-out period + if (usingNew) { + final IMessage.Kind[] NOSKIPS = new IMessage.Kind[0]; + IMessage.Kind[] skipActual = new IMessage.Kind[] { IMessage.INFO }; + int expectedInfo + = MessageUtil.numMessages(expected, IMessage.INFO, false); + if (0 < expectedInfo) { + // fyi, when expecting any info messages, have to expect all + skipActual = NOSKIPS; + } + messages = Diffs.makeDiffs( + "message", + (IMessage[]) expected.toArray(new IMessage[0]), + (IMessage[]) actual.toArray(new IMessage[0]), + NOSKIPS, + skipActual); + } else { + messages = Diffs.makeDiffs( + "message", + expected, + actual, + COMP_IMessage, + Diffs.ACCEPT_ALL, + CompilerDiffs.SKIP_UNEXPECTED_INFO); + } + Diffs recompiled = + Diffs.makeDiffs( + "recompiled", + expectedRecompiled, + actualRecompiled, + COMP_File); + diffs = new CompilerDiffs(messages, recompiled); + } + return diffs; + } + + /** + * 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() { + return !getCompilerDiffs().different; + } + + /** @return true if we are expecting the command to fail - i.e., any expected errors */ + public boolean expectingCommandTrue() { + return expectingCommandTrue; + } + + /** + * 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"); + } + // Report all messages except expected fail+ messages, + // which will cause the reported-to handler client to gack. + // XXX need some verbose way to report even expected fail+ + final boolean fastFail = false; // do all messages + if (0 == numExpectedFailed) { + MessageUtil.handleAll(handler, this, fastFail); + } else { + IMessage[] ra = getMessagesWithoutExpectedFails(); + MessageUtil.handleAll(handler, ra, fastFail); + } + + CompilerDiffs diffs = getCompilerDiffs(); + if (diffs.different) { + diffs.messages.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 message ", EOL, diffs.messages.unexpected); + render(sb, " missing message ", EOL, diffs.messages.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 + } + + /** + * 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 + */ + private boolean expecting(IMessage message) { + boolean match = false; + if (null != message) { + for (Iterator iter = expectedMessagesAsList.iterator(); + iter.hasNext(); + ) { + // amc - we have to compare against all messages to consume multiple + // text matches on same line. Return true if any matches. + if (0 == COMP_IMessage.compare(message, iter.next())) { + match = true; + } + } + } + if (!match) { + diffs = null; + } + return match; + } + + private IMessage[] getMessagesWithoutExpectedFails() { + IMessage[] result = super.getMessages(null, true); + // remove all expected fail+ (COSTLY) + ArrayList list = new ArrayList<>(); + int leftToFilter = numExpectedFailed; + for (int i = 0; i < result.length; i++) { + if ((0 == leftToFilter) + || !IMessage.FAIL.isSameOrLessThan(result[i].getKind())) { + list.add(result[i]); + } else { + // see if this failure was expected + if (expectedMessagesHasMatchFor(result[i])) { + leftToFilter--; // ok, don't add + } else { + list.add(result[i]); + } + } + } + result = (IMessage[]) list.toArray(new IMessage[0]); + return result; + } + + /** + * @param actual the actual IMessage to seek a match for in expected messages + * @return true if actual message is matched in the expected messages + */ + private boolean expectedMessagesHasMatchFor(IMessage actual) { + for (Iterator iter = expectedMessagesAsList.iterator(); + iter.hasNext(); + ) { + IMessage expected = (IMessage) iter.next(); + if (0 == COMP_IMessage.compare(expected, actual)) { + return true; + } + } + return false; + } + + /** @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 { + /** Skip info messages when reporting unexpected messages */ + static final Diffs.Filter SKIP_UNEXPECTED_INFO = new Diffs.Filter() { + public boolean accept(Object o) { + return ((o instanceof IMessage) && !((IMessage) o).isInfo()); + } + }; + public final Diffs messages; + public final Diffs recompiled; + public final boolean different; + + public CompilerDiffs(Diffs messages, Diffs recompiled) { + this.recompiled = recompiled; + this.messages = messages; + different = (messages.different || recompiled.different); + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/AjcTest.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/AjcTest.java new file mode 100644 index 000000000..bdebae02c --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/AjcTest.java @@ -0,0 +1,586 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.*; +import java.io.File; +import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +//import org.aspectj.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; + +/** + * An AjcTest has child subruns (compile, [inc-compile|run]*). + * XXX title keys shared between all instances + * (add Thread to key to restrict access?) + */ +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, e.g., as + *

    -ajctest[Require|Skip]Keywords=keyword{,keyword}..
    . + * See VALID_SUFFIXES for complete list. + */ + 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 staging, skip badInput, + * 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 TITLE_LIST = "TitleList="; + private static final String TITLE_FAIL_LIST = "TitleFailList="; + private static final String TITLE_CONTAINS= "TitleContains="; + 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[] + { TITLE_LIST, TITLE_FAIL_LIST, TITLE_CONTAINS, + REQUIRE_KEYWORDS, SKIP_KEYWORDS, PICK_PR })); + + /** Map String titlesName to List (String) of titles to accept */ + private static final Map> TITLES = new HashMap>(); + + private static List getTitles(String titlesName) { + return getTitles(titlesName, false); + } + private static List getTitles(String titlesName, boolean fail) { + if (LangUtil.isEmpty(titlesName)) { + return Collections.emptyList(); + } + List result = (List) TITLES.get(titlesName); + if (null == result) { + result = makeTitlesList(titlesName, fail); + TITLES.put(titlesName, result); + } + return result; + } + + /** + * Make titles list per titlesKey, either a path to a file + * containing "[PASS|FAIL] {title}(..)" entries, + * or a comma-delimited list of titles. + * @param titlesKey a String, either a path to a file + * containing "[PASS|FAIL] {title}(..)" entries, + * or a comma-delimited list of titles. + * @param fail if true, only read titles prefixed "FAIL" from files + * @return the unmodifiable List of titles (maybe empty, never null) + */ + private static List makeTitlesList(String titlesKey, boolean fail) { + File file = new File(titlesKey); + return file.canRead() + ? readTitlesFile(file, fail) + : parseTitlesList(titlesKey); + } + + /** + * Parse list of titles from comma-delmited list + * titlesList, trimming each entry and permitting + * comma to be escaped with '\'. + * @param titlesList a comma-delimited String of titles + * @return the unmodifiable List of titles (maybe empty, never null) + */ + private static List parseTitlesList(String titlesList) { + ArrayList result = new ArrayList(); + String last = null; + StringTokenizer st = new StringTokenizer(titlesList, ","); + while (st.hasMoreTokens()) { + String next = st.nextToken().trim(); + if (next.endsWith("\\")) { + next = next.substring(0, next.length()-1); + if (null == last) { + last = next; + } else { + last += next; + } + next = null; + } else if (null != last) { + next = (last + next).trim(); + last = null; + } else { + next = next.trim(); + } + if (!LangUtil.isEmpty(next)) { + result.add(next); + } + } + if (null != last) { + String m = "unterminated entry \"" + last; // XXX messages + System.err.println(m + "\" in " + titlesList); + result.add(last.trim()); + } + return Collections.unmodifiableList(result); + } + + /** + * Read titles from a test result file, accepting + * only those prefixed with [PASS|FAIL] and + * excluding the "[PASS|FAIL] Suite.Spec(.." entry. + * @param titlesFile the File containing a + * list of titles from test results, + * with some lines of the form + * [PASS|FAIL] {title}() (excluding + * [PASS|FAIL] Suite.Spec(.... + * @param titlesFile the File path to the file containing titles + * @param fail if true, only select titles prefixed "FAIL" + * @return the unmodifiable List of titles (maybe empty, never null) + */ + private static List readTitlesFile(File titlesFile, boolean fail) { + ArrayList result = new ArrayList(); + Reader reader = null; + try { + reader = new FileReader(titlesFile); + BufferedReader lines = new BufferedReader(reader); + String line; + while (null != (line = lines.readLine())) { + if ((line.startsWith("FAIL ") + || (!fail && line.startsWith("PASS "))) + && (!line.substring(5).startsWith("Suite.Spec("))) { + String title = line.substring(5); + int loc = title.lastIndexOf("("); + if (-1 != loc) { + title = title.substring(0, loc); + } + result.add(title); + } + } + } catch (IOException e) { + System.err.println("ignoring titles in " + titlesFile); // XXX messages + e.printStackTrace(System.err); + } finally { + if (null != reader) { + try { + reader.close(); + } catch (IOException e) { + // ignore + } + } + } + return Collections.unmodifiableList(result); + } + + /** 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); + } + + protected void initClone(Spec spec) + throws CloneNotSupportedException { + super.initClone(spec); + spec.bugId = bugId; + spec.suiteDir = suiteDir; + spec.testDirOffset = testDirOffset; + } + + public Object clone() throws CloneNotSupportedException { + Spec result = new Spec(); + initClone(result); + return result; + } + + public void setSuiteDir(File suiteDir) { + this.suiteDir = suiteDir; + } + + public File getSuiteDir() { + return suiteDir; + } + + /** @param bugId 100..999999 */ + public void setBugId(int bugId) { + LangUtil.throwIaxIfFalse((bugId > 10) && (bugId < 1000000), "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 = XMLWriter.makeAttribute("dir", value); + if (0 != bugId) { + attr += " " + XMLWriter.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 + *
      + *
    • the spec has a keyword the parent wants to skip
    • + *
    • the spec does not have a required keyword
    • + *
    • the spec does not have a required bugId
    • + *
    • the spec does not have a required title (description)n
    • + *
    + * When skipping, this issues a messages as to why skipped. + * Skip combinations are not guaranteed to work correctly. XXX + * @return false if this wants to be skipped, true otherwise + * @throws Error if selected option is not of the form + *
    -ajctest[Require|Skip]Keywords=keyword{,keyword}..
    . + */ + 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; + List permittedTitles = null; + List permittedTitleStrings = null; + 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(TITLE_LIST)) { + option = option.substring(TITLE_LIST.length()); + permittedTitles = getTitles(option); + } else if (option.startsWith(TITLE_FAIL_LIST)) { + option = option.substring(TITLE_FAIL_LIST.length()); + permittedTitles = getTitles(option, true); + } else if (option.startsWith(TITLE_CONTAINS)) { + option = option.substring(TITLE_CONTAINS.length()); + permittedTitleStrings = getTitles(option); + } 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 + "...)"); + } + if (null != permittedTitleStrings) { + boolean gotHit = false; + for (Iterator iter = permittedTitleStrings.iterator(); + !gotHit && iter.hasNext(); + ) { + String substring = (String) iter.next(); + if (-1 != this.description.indexOf(substring)) { + gotHit = true; + } + } + if (!gotHit) { + String reason = "title " + + this.description + + " does not contain any of " + + option; + skipMessage(handler, reason); + return false; + } + } else if (null != permittedTitles) { + if (!permittedTitles.contains(this.description)) { + String reason = "titlesList " + + option + + " did not contain " + + this.description; + skipMessage(handler, reason); + return false; + } + } else { + // all other options handled as comma-delimited lists + 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() + havePr = null; + } + } 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; + + /** + * Count the number of AjcTest in this suite. + * @param spec + * @return + */ + public static int countTests(Suite.Spec spec) { + return spec.children.size(); + } + + public static AjcTest.Spec[] getTests(Suite.Spec spec) { + if (null == spec) { + return new AjcTest.Spec[0]; + } + return (AjcTest.Spec[]) spec.children.toArray(new AjcTest.Spec[0]); + } + + 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 staging, skip badInput, + * 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 + } + + public Object clone() throws CloneNotSupportedException { + Spec spec = new Spec(); + super.initClone(spec); + spec.suiteDir = suiteDir; + return spec; + } + + /** @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; + } + + /** @return 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/test/java/org/aspectj/testing/harness/bridge/CompilerRun.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/CompilerRun.java new file mode 100644 index 000000000..8d6c587fe --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/CompilerRun.java @@ -0,0 +1,1840 @@ +/* ******************************************************************* + * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC), + * 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * Wes Isberg 2003 updates + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.*; +import java.lang.reflect.Array; +import java.util.*; + +import org.aspectj.bridge.*; +import org.aspectj.testing.ajde.CompileCommand; +import org.aspectj.testing.run.*; +import org.aspectj.testing.taskdefs.AjcTaskCompileCommand; +import org.aspectj.testing.util.options.*; +import org.aspectj.testing.util.options.Option.*; +import org.aspectj.testing.xml.*; +import org.aspectj.util.*; + +/** + * Run the compiler once. + * The lifecycle is as follows: + *
      + *
    • Spec (specification) is created.
    • + *
    • This is created using the Spec.
    • + *
    • setupAjcRun(Sandbox, Validator) is invoked, + * at which point this populates the shared sandbox + * with values derived from the spec and also + * sets up internal state based on both the sandbox + * and the spec.
    • + *
    • run(IRunStatus) is invoked, and this runs the compiler + * based on internal state, the spec, and the sandbox.
    • + *
    + * Programmer notes: + *
      + *
    • Paths are resolved absolutely, which fails to test the + * compiler's ability to find files relative to a source base
    • + *
    • This does not enforce the lifecycle.
    • + *
    • This must be used as the initial compile + * before doing an incremental compile. + * In that case, staging must be enabled.
    • + *
    + */ +public class CompilerRun implements IAjcRun { + // static final String JAVAC_COMPILER + // = JavacCompileCommand.class.getName(); + + 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 inpath option. + * The list is set up in setupAjcRun(..), + * which extracts only directories from the files attribute. + */ + final List inpaths; + + private CompilerRun(Spec spec) { + if (null == spec) { + throw new IllegalArgumentException("null spec"); + } + this.spec = spec; + arguments = new ArrayList(); + injars = new ArrayList(); + inpaths = new ArrayList(); + } + + + /** + * Select from input String[] if readable directories + * @param inputs String[] of input - null ignored + * @param baseDir the base directory of the input + * @return String[] of input that end with any input + */ + public static String[] selectDirectories(String[] inputs, File baseDir) { + if (LangUtil.isEmpty(inputs)) { + return new String[0]; + } + ArrayList result = new ArrayList(); + for (int i = 0; i < inputs.length; i++) { + String input = inputs[i]; + if (null == input) { + continue; + } + File inputFile = new File(baseDir, input); + if (inputFile.canRead() && inputFile.isDirectory()) { + result.add(input); + } + } + return (String[]) result.toArray(new String[0]); + } + + /** + * Select from input String[] based on suffix-matching + * @param inputs String[] of input - null ignored + * @param suffixes String[] of suffix selectors - null ignored + * @param ignoreCase if true, ignore case + * @return String[] of input that end with any input + */ + public static String[] endsWith(String[] inputs, String[] suffixes, boolean ignoreCase) { + if (LangUtil.isEmpty(inputs) || LangUtil.isEmpty(suffixes)) { + return new String[0]; + } + if (ignoreCase) { + String[] temp = new String[suffixes.length]; + for (int i = 0; i < temp.length; i++) { + String suff = suffixes[i]; + temp[i] = (null == suff ? null : suff.toLowerCase()); + } + suffixes = temp; + } + ArrayList result = new ArrayList(); + for (int i = 0; i < inputs.length; i++) { + String input = inputs[i]; + if (null == input) { + continue; + } + if (!ignoreCase) { + input = input.toLowerCase(); + } + for (int j = 0; j < suffixes.length; j++) { + String suffix = suffixes[j]; + if (null == suffix) { + continue; + } + if (input.endsWith(suffix)) { + result.add(input); + break; + } + } + } + return (String[]) result.toArray(new String[0]); + } + + /** + * This checks that the spec is reasonable and does setup: + *
      + *
    • calculate and set sandbox testBaseSrcDir as {Sandbox.testBaseDir}/ + * {Spec.testSrcDirOffset}/
    • + *
    • get the list of source File to compile as {Sandbox.testBaseSrcDir} / + * {Spec.getPaths..}
    • + *
    • get the list of extraClasspath entries to add to default classpath as + * {Sandbox.testBaseSrcDir} / {Spec.classpath..}
    • + *
    • get the list of aspectpath entries to use as the aspectpath as + * {Sandbox. testBaseSrcDir} / {Spec.aspectpath..}
    • + *
    + * All sources must be readable at this time, + * unless spec.badInput is true (for invalid-input tests). + * If staging, the source files and source roots are copied + * to a separate staging directory so they can be modified + * for incremental tests. Note that (as of this writing) the + * compiler only handles source roots for incremental tests. + * @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")) { + return false; + } + + this.sandbox = sandbox; + + String rdir = spec.testSrcDirOffset; + File testBaseSrcDir; + if ((null == rdir) || (0 == rdir.length())) { + testBaseSrcDir = sandbox.testBaseDir; + } else { + testBaseSrcDir = new File(sandbox.testBaseDir, rdir); + // XXX what if rdir is two levels deep? + if (!validator + .canReadDir(testBaseSrcDir, "sandbox.testBaseSrcDir")) { + return false; + } + } + + // 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[] inpathPaths; + final String[] injarPaths; + final String[] srcPaths; + { + final String[] paths = spec.getPathsArray(); + srcPaths = + endsWith( + paths, + CompilerRun.SOURCE_SUFFIXES, + true); + injarPaths = + endsWith(paths, CompilerRun.JAR_SUFFIXES, true); + inpathPaths = + selectDirectories(paths, testBaseSrcDir); + if (!spec.badInput) { + int found = inpathPaths.length + injarPaths.length + srcPaths.length; + if (paths.length != found) { + validator.fail("found " + found + " of " + paths.length + " sources"); + } + } + } + + // validate readable for sources + if (!spec.badInput) { + if (!validator.canRead(testBaseSrcDir, srcPaths, "sources") + // see validation of inpathPaths below due to ambiguous base dir + || !validator.canRead( + testBaseSrcDir, + spec.argfiles, + "argfiles") + || !validator.canRead( + testBaseSrcDir, + spec.classpath, + "classpath") + || !validator.canRead( + testBaseSrcDir, + spec.aspectpath, + "aspectpath") + || !validator.canRead( + testBaseSrcDir, + spec.sourceroots, + "sourceroots") + || !validator.canRead( + testBaseSrcDir, + spec.extdirs, + "extdirs")) { + return false; + } + } + + int numSources = + srcPaths.length + + injarPaths.length + + inpathPaths.length + + spec.argfiles.length + + spec.sourceroots.length; + if (!spec.badInput && (numSources < 1)) { + validator.fail( + "no input jars, arg files, or source files or roots"); + return false; + } + + final File[] argFiles = + FileUtil.getBaseDirFiles(testBaseSrcDir, spec.argfiles); + final File[] injarFiles = + FileUtil.getBaseDirFiles(testBaseSrcDir, injarPaths); + final File[] inpathFiles = + FileUtil.getBaseDirFiles(testBaseSrcDir, inpathPaths); + final File[] aspectFiles = + FileUtil.getBaseDirFiles(testBaseSrcDir, spec.aspectpath); + final File[] extdirFiles = + FileUtil.getBaseDirFiles(testBaseSrcDir, spec.extdirs); + final File[] classFiles = + FileUtil.getBaseDirFiles(testBaseSrcDir, spec.classpath); + final File[] xlintFiles = (null == spec.xlintfile ? new File[0] + : FileUtil.getBaseDirFiles(testBaseSrcDir, new String[] {spec.xlintfile})); + + // injars might be outjars in the classes dir... + for (int i = 0; i < injarFiles.length; i++) { + if (!injarFiles[i].exists()) { + injarFiles[i] = new File(sandbox.classesDir, injarPaths[i]); + } + } + for (int i = 0; i < inpathFiles.length; i++) { + if (!inpathFiles[i].exists()) { + inpathFiles[i] = new File(sandbox.classesDir, inpathPaths[i]); + } + } + // moved after solving any injars that were outjars + if (!validator.canRead(injarFiles, "injars") + || !validator.canRead(injarFiles, "injars")) { + return false; + } + + // hmm - duplicates validation above, verifying getBaseDirFiles? + if (!spec.badInput) { + if (!validator.canRead(argFiles, "argFiles") + || !validator.canRead(injarFiles, "injarFiles") + || !validator.canRead(inpathFiles, "inpathFiles") + || !validator.canRead(aspectFiles, "aspectFiles") + || !validator.canRead(classFiles, "classFiles") + || !validator.canRead(xlintFiles, "xlintFiles")) { + return false; + } + } + + final File[] srcFiles; + File[] sourcerootFiles = new File[0]; + // source text files are copied when staging incremental tests + if (!spec.isStaging()) { + // XXX why this? was always? || (testBaseSrcDir != sandbox.stagingDir))) { + srcFiles = + FileUtil.getBaseDirFiles( + testBaseSrcDir, + srcPaths, + CompilerRun.SOURCE_SUFFIXES); + if (!LangUtil.isEmpty(spec.sourceroots)) { + sourcerootFiles = + FileUtil.getBaseDirFiles( + testBaseSrcDir, + spec.sourceroots, + null); + } + } else { // staging - copy files + if (spec.badInput) { + validator.info( + "badInput ignored - files checked when staging"); + } + try { + // copy all files, then remove tagged ones + // XXX make copyFiles support a filter? + srcFiles = + FileUtil.copyFiles( + testBaseSrcDir, + srcPaths, + sandbox.stagingDir); + if (!LangUtil.isEmpty(spec.sourceroots)) { + sourcerootFiles = + FileUtil.copyFiles( + testBaseSrcDir, + spec.sourceroots, + sandbox.stagingDir); + // delete incremental files in sourceroot after copying // XXX inefficient + // an incremental file has an extra "." in name + // most .java files don't, because they are named after + // the principle type they contain, and simple type names + // have no dots. + FileFilter pickIncFiles = new FileFilter() { + public boolean accept(File file) { + if (file.isDirectory()) { + // continue recursion + return true; + } + String path = file.getPath(); + // only source files are relevant to staging + if (!FileUtil.hasSourceSuffix(path)) { + return false; + } + int first = path.indexOf("."); + int last = path.lastIndexOf("."); + return (first != last); + } + }; + for (int i = 0; i < sourcerootFiles.length; i++) { + FileUtil.deleteContents( + sourcerootFiles[i], + pickIncFiles, + false); + } + if (0 < sourcerootFiles.length) { + FileUtil.sleepPastFinalModifiedTime( + sourcerootFiles); + } + } + File[] files = + FileUtil.getBaseDirFiles(sandbox.stagingDir, srcPaths); + if (0 < files.length) { + FileUtil.sleepPastFinalModifiedTime(files); + } + } catch (IllegalArgumentException e) { + validator.fail("staging - bad input", e); + return false; + } catch (IOException e) { + validator.fail("staging - operations", e); + return false; + } + } + if (!spec.badInput + && !validator.canRead(srcFiles, "copied paths")) { + return false; + } + arguments.clear(); + + if (!LangUtil.isEmpty(xlintFiles)) { + arguments.add("-Xlintfile"); + String sr = FileUtil.flatten(xlintFiles, null); + arguments.add(sr); + } + if (spec.outjar != null) { + arguments.add("-outjar"); + arguments.add(new File(sandbox.classesDir,spec.outjar).getPath()); + } + if (!LangUtil.isEmpty(extdirFiles)) { + arguments.add("-extdirs"); + String sr = FileUtil.flatten(extdirFiles, null); + arguments.add(sr); + } + if (!LangUtil.isEmpty(sourcerootFiles)) { + arguments.add("-sourceroots"); + String sr = FileUtil.flatten(sourcerootFiles, null); + arguments.add(sr); + } + if (!LangUtil.isEmpty(srcFiles)) { + arguments.addAll(Arrays.asList(FileUtil.getPaths(srcFiles))); + } + injars.clear(); + if (!LangUtil.isEmpty(injarFiles)) { + injars.addAll(Arrays.asList(FileUtil.getPaths(injarFiles))); + } + inpaths.clear(); + if (!LangUtil.isEmpty(inpathFiles)) { + inpaths.addAll(Arrays.asList(FileUtil.getPaths(inpathFiles))); + } + if (!LangUtil.isEmpty(argFiles)) { + String[] ra = FileUtil.getPaths(argFiles); + for (int j = 0; j < ra.length; j++) { + arguments.add("@" + ra[j]); + } + if (!spec.badInput && spec.isStaging) { + validator.fail( + "warning: files listed in argfiles not staged"); + } + } + + // save classpath and aspectpath in sandbox for this and other clients + final boolean checkReadable = !spec.badInput; + int size = spec.includeClassesDir ? 3 : 2; + File[] cp = new File[size + classFiles.length]; + System.arraycopy(classFiles, 0, cp, 0, classFiles.length); + int index = classFiles.length; + if (spec.includeClassesDir) { + cp[index++] = sandbox.classesDir; + } + cp[index++] = Globals.F_aspectjrt_jar; + cp[index++] = Globals.F_testingclient_jar; + sandbox.compilerRunInit(this, testBaseSrcDir, aspectFiles, + checkReadable, cp, checkReadable, null); + + // XXX todo set bootclasspath if set for forking? + return true; + } + + /** + * Setup result evaluation and command line, run, and evaluate result. + *
  • setup an AjcMessageHandler using the expected messages from + * {@link Spec#getMessages()}.
  • + *
  • heed any globals interpreted into a TestSetup by reading + * {@link Spec@getOptions()}. For a list of supported globals, see + * {@link setupArgs(ArrayList, IMessageHandler}.
  • + *
  • construct a command line, using as classpath + * {@link Sandbox.classpathToString()}
  • + *
  • construct a compiler using {@link Spec#compiler} + * or any overriding value set in TestSetup.
  • + *
  • Just before running, set the compiler in the sandbox using + * {@link Sandbox.setCompiler(ICommand)}.
  • + *
  • 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.
  • + * @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 { + if (spec.outjar == null) { + 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); + } + String path = sandbox.classpathToString(this); + if (!LangUtil.isEmpty(path)) { + argList.add("-classpath"); + argList.add(path); + } + path = sandbox.getBootclasspath(this); + if (!LangUtil.isEmpty(path)) { + argList.add("-bootclasspath"); + argList.add(path); + } + + path = sandbox.aspectpathToString(this); + if (!LangUtil.isEmpty(path)) { + argList.add("-aspectpath"); + argList.add(path); + } + + if (0 < injars.size()) { + argList.add("-injars"); + argList.add( + FileUtil.flatten( + (String[]) injars.toArray(new String[0]), + null)); + } + + if (0 < inpaths.size()) { + argList.add("-inpath"); + argList.add( + FileUtil.flatten( + (String[]) inpaths.toArray(new String[0]), + null)); + } + + // put specified arguments last, for better badInput tests + argList.addAll(setupResult.commandOptions); + + // add both java/aspectj and argfiles + argList.addAll(arguments); + + // XXX hack - seek 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 = spec.reuseCompiler + // throws IllegalStateException if null + ? sandbox.getCommand(this) + : ReflectionFactory.makeCommand(setupResult.compilerName, status); + DirChanges dirChanges = null; + if (null == compiler) { + MessageUtil.fail( + status, + "unable to make compiler " + setupResult.compilerName); + return false; + } else { + if (setupResult.compilerName != Spec.DEFAULT_COMPILER) { + MessageUtil.info( + status, + "compiler: " + setupResult.compilerName); + } + 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); + } + } + handlerResult = handler.passed(); + 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 + ")"; + } + + /** + * 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"; + public static final String DEFAULT_COMPILER = + ReflectionFactory.ECLIPSE; + static final String SEEK_PREFIX = "-seek:"; + static final String SEEK_MESSAGE_PREFIX = "found: "; + + private static final CRSOptions CRSOPTIONS = new CRSOptions(); + /** + * Retitle description to title, paths to files, do comment, + * staging, badInput, + * do dirChanges, and print no chidren. + */ + private static final AbstractRunSpec.XMLNames NAMES = + new AbstractRunSpec.XMLNames( + AbstractRunSpec.XMLNames.DEFAULT, + "title", + null, + null, + null, + "files", + null, + null, + null, + false, + false, + true); + + /** + * If the source version warrants, add a -bootclasspath + * entry to the list of arguments to add. + * This will fail and return an error String if the + * required library is not found. + * @param sourceVersion the String (if any) describing the -source option + * (expecting one of [null, "1.3", "1.4", "1.5"]. + * @param compilerName the String name of the target compiler + * @param toAdd the ArrayList to add -bootclasspath to + * @return the String describing any errors, or null if no errors + */ + private static String updateBootclasspathForSourceVersion( + String sourceVersion, + String compilerName, + ArrayList toAdd) { + if (null == sourceVersion) { + return null; + } + if (3 != sourceVersion.length()) { + throw new IllegalArgumentException( + "bad version: " + sourceVersion); + } + if (null == toAdd) { + throw new IllegalArgumentException("null toAdd"); + } + int version = sourceVersion.charAt(2) - '0'; + switch (version) { + case (3) : + if (Globals.supportsJava("1.4")) { + if (!FileUtil.canReadFile(Globals.J2SE13_RTJAR)) { + return "no 1.3 libraries to handle -source 1.3"; + } + toAdd.add("-bootclasspath"); + toAdd.add(Globals.J2SE13_RTJAR.getAbsolutePath()); + } + break; + case (4) : + if (!Globals.supportsJava("1.4")) { + if (ReflectionFactory + .ECLIPSE + .equals(compilerName)) { + return "run eclipse under 1.4 to handle -source 1.4"; + } + if (!FileUtil.canReadFile(Globals.J2SE14_RTJAR)) { + return "no 1.4 libraries to handle -source 1.4"; + } + toAdd.add("-bootclasspath"); + toAdd.add(Globals.J2SE14_RTJAR.getAbsolutePath()); + } + break; + case (5) : + return "1.5 not supported in CompilerRun"; + case (0) : + // ignore - no version specified + break; + default : + throw new Error("unexpected version: " + version); + } + return null; + } + + static CRSOptions testAccessToCRSOptions() { + return CRSOPTIONS; + } + + static Options testAccessToOptions() { + return CRSOPTIONS.getOptions(); + } + + private static String[] copy(String[] input) { + if (null == input) { + return null; + } + String[] result = new String[input.length]; + System.arraycopy(input, 0, result, 0, input.length); + return result; + } + + protected String compiler; + + // use same command - see also IncCompiler.Spec.fresh + protected boolean reuseCompiler; + protected boolean permitAnyCompiler; + protected boolean includeClassesDir; + + protected TestSetup testSetup; + + protected String[] argfiles = new String[0]; + protected String[] aspectpath = new String[0]; + protected String[] classpath = new String[0]; + protected String[] sourceroots = new String[0]; + protected String[] extdirs = new String[0]; + + /** src path = {suiteParentDir}/{testBaseDirOffset}/{testSrcDirOffset}/{path} */ + protected String testSrcDirOffset; + protected String xlintfile; + protected String outjar; + + public Spec() { + super(XMLNAME); + setXMLNames(NAMES); + compiler = DEFAULT_COMPILER; + } + + protected void initClone(Spec spec) + throws CloneNotSupportedException { + super.initClone(spec); + spec.argfiles = copy(argfiles); + spec.aspectpath = copy(aspectpath); + spec.classpath = copy(classpath); + spec.compiler = compiler; + spec.includeClassesDir = includeClassesDir; + spec.reuseCompiler = reuseCompiler; + spec.permitAnyCompiler = permitAnyCompiler; + spec.sourceroots = copy(sourceroots); + spec.extdirs = copy(extdirs); + spec.outjar = outjar; + spec.testSetup = null; + if (null != testSetup) { + spec.testSetup = (TestSetup) testSetup.clone(); + } + spec.testSrcDirOffset = testSrcDirOffset; + } + + public Object clone() throws CloneNotSupportedException { + Spec result = new Spec(); + initClone(result); + return result; + } + + public void setIncludeClassesDir(boolean include) { + this.includeClassesDir = include; + } + public void setReuseCompiler(boolean reuse) { + this.reuseCompiler = reuse; + } + + public void setPermitAnyCompiler(boolean permitAny) { + this.permitAnyCompiler = permitAny; + } + + 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); + } + + 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); + } + + /** + * Add to default classpath + * (which includes aspectjrt.jar and testing-client.jar). + * @param files comma-delimited list of classpath entries - ignored if + * null or empty + */ + public void setClasspath(String files) { + if (!LangUtil.isEmpty(files)) { + classpath = XMLWriter.unflattenList(files); + } + } + + /** + * Set source roots, deleting any old ones + * @param files comma-delimited list of directories + * - ignored if null or empty + */ + public void setSourceroots(String dirs) { + if (!LangUtil.isEmpty(dirs)) { + sourceroots = XMLWriter.unflattenList(dirs); + } + } + public void setXlintfile(String path) { + xlintfile = path; + } + + public void setOutjar(String path) { + outjar = path; + } + + /** + * Set extension dirs, deleting any old ones + * @param files comma-delimited list of directories + * - ignored if null or empty + */ + public void setExtdirs(String dirs) { + if (!LangUtil.isEmpty(dirs)) { + extdirs = XMLWriter.unflattenList(dirs); + } + } + + /** + * Set aspectpath, deleting any old ones + * @param files comma-delimited list of aspect jars - 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[]) copy(argfiles); + } + + /** + * 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) { + LangUtil.throwIaxIfNull(source, "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; + } + + /** + * This implementation skips if: + *
      + *
    • incremental test, but using ajc (not eclipse)
    • + *
    • usejavac, but javac is not available on the classpath
    • + *
    • eclipse, but -usejavac or -preprocess test
    • + *
    • -source 1.4, but running under 1.2 (XXX design)
    • + *
    • local/global option conflicts (-lenient/-strict)
    • + *
    • semantic conflicts (e.g., -lenient/-strict)
    • + *
    + * @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 compilerClassName = compiler; + if (null != testSetup) { + compilerClassName = testSetup.compilerName; + } + if (null != compilerClassName) { + int loc = compilerClassName.lastIndexOf("."); + if (-1 != loc) { + compilerClassName = + compilerClassName.substring(loc + 1); + } + } + return compilerClassName; + } + + /** @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; + } + + protected String getPrintName() { + return "CompilerRun.Spec " + getShortCompilerName(); + } + + /** + * 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: + *
      + *
    • -foo: use -foo unless forced off.
    • + *
    • ^foo: (force off) remove any -foo option from the run flags
    • + *
    • !foo: (force on) require the -foo flag
    • + *
    + * 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. + *

    + * There are some flags which are interpreted by the test + * and removed from the list of flags passed to the command + * (see testFlag()): + *

      + *
    • eclipse: use the new eclipse compiler (can force)
    • + *
    • ajc: use the old ajc compiler (can force)
    • + *
    • ignoreWarnings: ignore warnings in result evaluations (no force)
    • + *
    + *

    + * There are some flags which are inconsistent with each other. + * These are treated as conflicts and the test is skipped: + *

      + *
    • lenient, strict
    • + *
    + *

    + *

    + * This also interprets any relevant System properties, + * e.g., from JavaRun.BOOTCLASSPATH_KEY. + *

    + * Finally, compiler limitations are enforced here by skipping + * tests which the compiler cannot do: + *

      + *
    • eclipse does not do -lenient, -strict, -usejavac, -preprocess, + * -XOcodeSize, -XSerializable, XaddSafePrefix, + * -XserializableAspects,-XtargetNearSource
    • + *
    • ajc does not run in incremental (staging) mode, + * nor with -usejavac if javac is not on the classpath
    • + *
    + * Errors:This will remove an arg not prefixed by [-|!|^] after + * providing an info message. + * TestSetup Result: + * 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. + * This means the test is skipped. + * @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 + final Spec spec = this; + final TestSetup result = new TestSetup(); + result.compilerName = spec.compiler; + // urk - s.b. null, but expected + Values values = gatherValues(result); + + if ((null == values) || (null != result.failureReason)) { + return checkResult(result); + } + + // send info messages about + // forced staging when -incremental + // or staging but no -incremental flag + Option.Family getFamily = + CRSOPTIONS.crsIncrementalOption.getFamily(); + final boolean haveIncrementalFlag = + (null != values.firstInFamily(getFamily)); + + if (spec.isStaging()) { + if (!haveIncrementalFlag) { + MessageUtil.info( + handler, + "staging but no -incremental flag"); + } + } else if (haveIncrementalFlag) { + spec.setStaging(true); + MessageUtil.info(handler, "-incremental forcing staging"); + } + + if (hasInvalidOptions(values, result)) { + return checkResult(result); + } + + // set compiler in result + getFamily = CRSOPTIONS.ajccompilerOption.getFamily(); + Option.Value compiler = values.firstInFamily(getFamily); + if (null != compiler) { + result.compilerName + = CRSOPTIONS.compilerClassName(compiler.option); + if (null == result.compilerName) { + result.failureReason = + "unable to get class name for " + compiler; + return checkResult(result); + } + } + String compilerName = + (null == result.compilerName + ? spec.compiler + : result.compilerName); + // check compiler semantics + if (hasCompilerSpecErrors(compilerName, values, result)) { + return checkResult(result); + } + // add toadd and finish result + ArrayList args = new ArrayList(); + String[] rendered = values.render(); + if (!LangUtil.isEmpty(rendered)) { + args.addAll(Arrays.asList(rendered)); + } + // update bootclasspath + getFamily = CRSOPTIONS.crsSourceOption.getFamily(); + Option.Value source = values.firstInFamily(getFamily); + if (null != source) { + String sourceVersion = source.unflatten()[1]; + ArrayList toAdd = new ArrayList(); + /*String err =*/ + updateBootclasspathForSourceVersion( + sourceVersion, + spec.compiler, + toAdd); + args.addAll(toAdd); + } + result.commandOptions = args; + result.result = true; + return checkResult(result); + } + + /** + * Ensure exit invariant: + * result.result == (null == result.failureReason) + * == (null != result.commandOptions) + * @param result the TestSetup to verify + * @return result + * @throws Error if invariant is not true + */ + TestSetup checkResult(TestSetup result) { + String err = null; + if (null == result) { + err = "null result"; + } else if (result.result != (null == result.failureReason)) { + err = + result.result + ? "expected no failure: " + result.failureReason + : "fail for no reason"; + } else if (result.result != (null != result.commandOptions)) { + err = + result.result + ? "expected command options" + : "unexpected command options"; + } + if (null != err) { + throw new Error(err); + } + return result; + } + + boolean hasInvalidOptions(Values values, TestSetup result) { + // not supporting 1.0 options any more + for (Iterator iter = CRSOPTIONS.invalidOptions.iterator(); + iter.hasNext(); + ) { + Option option = (Option) iter.next(); + if (null != values.firstOption(option)) { + result.failureReason = + "invalid option in harness: " + option; + return true; + } + } + return false; + } + + boolean hasCompilerSpecErrors( + String compilerName, + Values values, + TestSetup result) { + /* + * Describe any semantic conflicts between options. + * This skips: + * - old 1.0 options, including lenient v. strict + * - old ajc options, include !incremental and usejavac w/o javac + * - invalid eclipse options (mostly ajc) + * @param compilerName the String name of the target compiler + * @return a String describing any conflicts, or null if none + */ + if (!permitAnyCompiler + && (!(ReflectionFactory.ECLIPSE.equals(compilerName) + || ReflectionFactory.OLD_AJC.equals(compilerName) + || CRSOptions.AJDE_COMPILER.equals(compilerName) + || CRSOptions.AJCTASK_COMPILER.equals(compilerName) + || permitAnyCompiler + ))) { + //|| BUILDER_COMPILER.equals(compilerName)) + result.failureReason = + "unrecognized compiler: " + compilerName; + return true; + } + // not supporting ajc right now + if (null + != values.firstOption(CRSOPTIONS.ajccompilerOption)) { + result.failureReason = "ajc not supported"; + return true; + } + // not supporting 1.0 options any more + for (Iterator iter = CRSOPTIONS.ajc10Options.iterator(); + iter.hasNext(); + ) { + Option option = (Option) iter.next(); + if (null != values.firstOption(option)) { + result.failureReason = "old ajc 1.0 option: " + option; + return true; + } + } + + return false; + } + + protected Values gatherValues(TestSetup result) { + final Spec spec = this; + // ---- local option values + final Values localValues; + final Options options = CRSOPTIONS.getOptions(); + try { + String[] input = getOptionsArray(); + // this handles reading options, + // flattening two-String options, etc. + localValues = options.acceptInput(input); + // all local values should be picked up + String err = Options.missedMatchError(input, localValues); + if (!LangUtil.isEmpty(err)) { + result.failureReason = err; + return null; + } + } catch (InvalidInputException e) { + result.failureReason = e.getFullMessage(); + return null; + } + + // ---- global option values + StringBuffer errs = new StringBuffer(); + final Values globalValues = + spec.runtime.extractOptions(options, true, errs); + if (errs.length() > 0) { + result.failureReason = errs.toString(); + return null; + } + final Values combined = + Values.wrapValues( + new Values[] { localValues, globalValues }); + + String err = combined.resolve(); + if (null != err) { + result.failureReason = err; + return null; + } + + return handleTestArgs(combined, result); + } + + // final int len = globalValues.length() + localValues.length(); + // final Option.Value[] combinedValues = new Option.Value[len]; + // System.arraycopy( + // globalValues, + // 0, + // combinedValues, + // 0, + // globalValues.length()); + // System.arraycopy( + // localValues, + // 0, + // combinedValues, + // globalValues.length(), + // localValues.length()); + // + // result.compilerName = spec.compiler; + // if (0 < combinedValues.length) { + // // this handles option forcing, etc. + // String err = Options.resolve(combinedValues); + // if (null != err) { + // result.failureReason = err; + // return null; + // } + // if (!handleTestArgs(combinedValues, result)) { + // return null; + // } + // } + // return Values.wrapValues(combinedValues); + // } + /** + * This interprets and nullifies values for the test. + * @param values the Option.Value[] being processed + * @param result the TestSetup to modify + * @return false if error (caller should return), true otherwise + */ + Values handleTestArgs(Values values, final TestSetup result) { + final Option.Family compilerFamily = + CRSOPTIONS.ajccompilerOption.getFamily(); + Values.Selector selector = new Values.Selector() { + protected boolean accept(Option.Value value) { + if (null == value) { + return false; + } + Option option = value.option; + if (compilerFamily.sameFamily(option.getFamily())) { + if (value.prefix.isSet()) { + String compilerClass + = CRSOPTIONS.compilerClassName(option); + if (null == compilerClass) { + result.failureReason = + "unrecognized compiler: " + value; + throw Values.Selector.STOP; + } + if (!CRSOPTIONS.compilerIsLoadable(option)) { + result.failureReason = + "unable to load compiler: " + option; + throw Values.Selector.STOP; + } + result.compilerName = compilerClass; + } + return true; + } else if ( + CRSOPTIONS.crsIgnoreWarnings.sameOptionIdentifier( + option)) { + result.ignoreWarnings = value.prefix.isSet(); + result.ignoreWarningsSet = true; + return true; + } + return false; + } + }; + return values.nullify(selector); + } + + // /** + // * This interprets and nullifies values for the test. + // * @param values the Option.Value[] being processed + // * @param result the TestSetup to modify + // * @return false if error (caller should return), true otherwise + // */ + // boolean handleTestArgs(Option.Value[] values, TestSetup result) { + // if (!LangUtil.isEmpty(values)) { + // for (int i = 0; i < values.length; i++) { + // Option.Value value = values[i]; + // if (null == value) { + // continue; + // } + // Option option = value.option; + // if (option.sameOptionFamily(ECLIPSE_OPTION)) { + // if (!value.prefix.isSet()) { + // values[i] = null; + // continue; + // } + // String compilerClass = + // (String) COMPILER_OPTION_TO_CLASSNAME.get( + // option); + // if (null == compilerClass) { + // result.failureReason = + // "unrecognized compiler: " + value; + // return false; + // } + // result.compilerName = compilerClass; + // values[i] = null; + // } else if ( + // option.sameOptionFamily(crsIgnoreWarnings)) { + // result.ignoreWarnings = value.prefix.isSet(); + // result.ignoreWarningsSet = true; + // values[i] = null; + // } + // } + // } + // return true; + // } + + // 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) { + out.startElement(xmlElementName, false); + if (!LangUtil.isEmpty(testSrcDirOffset)) { + out.printAttribute("dir", testSrcDirOffset); + } + super.writeAttributes(out); + if (!DEFAULT_COMPILER.equals(compiler)) { + out.printAttribute("compiler", compiler); + } + if (reuseCompiler) { + out.printAttribute("reuseCompiler", "true"); + } +// test-only feature +// if (permitAnyCompiler) { +// out.printAttribute("permitAnyCompiler", "true"); +// } + if (includeClassesDir) { + out.printAttribute("includeClassesDir", "true"); + } + if (!LangUtil.isEmpty(argfiles)) { + out.printAttribute( + "argfiles", + XMLWriter.flattenFiles(argfiles)); + } + if (!LangUtil.isEmpty(aspectpath)) { + out.printAttribute( + "aspectpath", + XMLWriter.flattenFiles(aspectpath)); + } + if (!LangUtil.isEmpty(sourceroots)) { + out.printAttribute( + "sourceroots", + XMLWriter.flattenFiles(sourceroots)); + } + if (!LangUtil.isEmpty(extdirs)) { + out.printAttribute( + "extdirs", + XMLWriter.flattenFiles(extdirs)); + } + out.endAttributes(); + if (!LangUtil.isEmpty(dirChanges)) { + DirChanges.Spec.writeXml(out, dirChanges); + } + SoftMessage.writeXml(out, getMessages()); + 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 Object clone() { + TestSetup testSetup = new TestSetup(); + testSetup.compilerName = compilerName; + testSetup.ignoreWarnings = ignoreWarnings; + testSetup.ignoreWarningsSet = ignoreWarningsSet; + testSetup.result = result; + testSetup.failureReason = failureReason; + testSetup.seek = seek; + if (null != commandOptions) { + testSetup.commandOptions = new ArrayList(); + testSetup.commandOptions.addAll(commandOptions); + } + return testSetup; + } + public String toString() { + return "TestSetup(" + + (null == compilerName ? "" : compilerName + " ") + + (!ignoreWarningsSet + ? "" + : (ignoreWarnings ? "" : "do not ") + + "ignore warnings ") + + (result ? "" : "setup failed") + + ")"; + } + } + + /** + * Options-related stuff in the spec. + */ + static class CRSOptions { + // static final String BUILDER_COMPILER = + // "org.aspectj.ajdt.internal.core.builder.Builder.Command"; + static final String AJDE_COMPILER = + CompileCommand.class.getName(); + + static final String AJCTASK_COMPILER = + AjcTaskCompileCommand.class.getName(); + + private final Map compilerOptionToLoadable = new TreeMap(); + /* + * The options field in a compiler test permits some arbitrary + * command-line options to be set. It does not permit things + * like classpath, aspectpath, files, etc. which are set + * using other fields in the test specification, so the options + * permitted are a subset of those permitted on the command-line. + * + * Global options specified on the harness command-line are + * adopted for the compiler command-line if they are permitted + * in the options field. That means we have to detect each + * permitted option, rather than just letting all through + * for the compiler. + * + * Conversely, some options are targeted not at the compiler, + * but at the test itself (e.g., to ignore warnings, or to + * select a compiler. + * + * The harness can run many compilers, and they differ in + * which options are permitted. You can specify a compiler + * as an option (e.g., -eclipse). So the set of constraints + * on the list of permitted options can differ from test to test. + * + * The following code sets up the lists of permitted options + * and breaks out subsets for different compiler-variant checks. + * Most options are created but not named, but some options + * are named to detect corresponding values for further + * processing. e.g., the compiler options are saved so + * we can do per-compiler option verification. + * + */ + private final Options crsOptions; + private final Family compilerFamily; + private final Option crsIncrementalOption; + private final Option crsSourceOption; + // these are options handled/absorbed by CompilerRun + private final Option crsIgnoreWarnings; + private final Option eclipseOption; + private final Option buildercompilerOption; + private final Option ajdecompilerOption; + private final Option javacOption; + private final Option ajctaskcompilerOption; + private final Option ajccompilerOption; + private final Map compilerOptionToClassname; + private final Set compilerOptions; + // compiler verification - permit but flag ajc 1.0 options + private final List ajc10Options; + private final List invalidOptions; + + private CRSOptions() { + crsOptions = new Options(true); + Option.Factory factory = new Option.Factory("CompilerRun"); + // compiler options go in map + eclipseOption = + factory.create( + "eclipse", + "compiler", + Option.FORCE_PREFIXES, + false); + compilerFamily = eclipseOption.getFamily(); + buildercompilerOption = + factory.create( + "builderCompiler", + "compiler", + Option.FORCE_PREFIXES, + false); + ajctaskcompilerOption = + factory.create( + "ajctaskCompiler", + "compiler", + Option.FORCE_PREFIXES, + false); + ajdecompilerOption = + factory.create( + "ajdeCompiler", + "compiler", + Option.FORCE_PREFIXES, + false); + ajccompilerOption = + factory.create( + "ajc", + "compiler", + Option.FORCE_PREFIXES, + false); + javacOption = + factory.create( + "javac", + "compiler", + Option.FORCE_PREFIXES, + false); + + Map map = new TreeMap(); + map.put(eclipseOption, ReflectionFactory.ECLIPSE); + //map.put(BUILDERCOMPILER_OPTION, BUILDER_COMPILER); + map.put( + ajctaskcompilerOption, + AJCTASK_COMPILER); + map.put(ajdecompilerOption, AJDE_COMPILER); + map.put(ajccompilerOption, ReflectionFactory.OLD_AJC); + //map.put(JAVAC_OPTION, "XXX javac option not supported"); + compilerOptionToClassname = + Collections.unmodifiableMap(map); + + compilerOptions = + Collections.unmodifiableSet( + compilerOptionToClassname.keySet()); + // options not permitted in the harness + List list = new ArrayList(); + list.add(factory.create("workingdir")); + list.add(factory.create("argfile")); + list.add(factory.create("sourceroots")); + list.add(factory.create("outjar")); + invalidOptions = Collections.unmodifiableList(list); + + // other options added directly + crsIncrementalOption = factory.create("incremental"); + + crsIgnoreWarnings = factory.create("ignoreWarnings"); + + crsSourceOption = + factory + .create( + "source", + "source", + Option.FORCE_PREFIXES, + false, + new String[][] { new String[] { "1.3", "1.4", "1.5" } + }); + + // ajc 1.0 options + // workingdir above in invalid options + list = new ArrayList(); + list.add(factory.create("usejavac")); + list.add(factory.create("preprocess")); + list.add(factory.create("nocomment")); + list.add(factory.create("porting")); + list.add(factory.create("XOcodeSize")); + list.add(factory.create("XTargetNearSource")); + list.add(factory.create("XaddSafePrefix")); + list.add( + factory.create( + "lenient", + "lenient", + Option.FORCE_PREFIXES, + false)); + list.add( + factory.create( + "strict", + "lenient", + Option.FORCE_PREFIXES, + false)); + ajc10Options = Collections.unmodifiableList(list); + + // -warn:.. and -g/-g:.. are not exclusive + if (!(factory.setupFamily("debug", true) + && factory.setupFamily("warning", true))) { + System.err.println("CompilerRun debug/warning fail!"); + } + Option[] options = + new Option[] { + crsIncrementalOption, + crsIgnoreWarnings, + crsSourceOption, + factory.create( + "Xlint", + "XLint", + Option.FORCE_PREFIXES, + true), + factory.create("verbose"), + factory.create("emacssym"), + factory.create("referenceInfo"), + factory.create("nowarn"), + factory.create("deprecation"), + factory.create("noImportError"), + factory.create("proceedOnError"), + factory.create("preserveAllLocals"), + factory.create( + "warn", + "warning", + Option.STANDARD_PREFIXES, + true), + factory.create( + "g", + "debug", + Option.STANDARD_PREFIXES, + false), + factory.create( + "g:", + "debug", + Option.STANDARD_PREFIXES, + true), + factory.create( + "1.3", + "compliance", + Option.FORCE_PREFIXES, + false), + factory.create( + "1.4", + "compliance", + Option.FORCE_PREFIXES, + false), + factory.create( + "1.5", + "compliance", + Option.FORCE_PREFIXES, + false), + factory + .create( + "target", + "target", + Option.FORCE_PREFIXES, + false, + new String[][] { new String[] { + "1.1", + "1.2", + "1.3", + "1.4", + "1.5" }}), + factory.create("XnoInline"), + factory.create("XterminateAfterCompilation"), + factory.create("Xreweavable"), + factory.create("XnotReweavable"), + factory.create("XserializableAspects") + }; + + // among options not permitted: extdirs... + + for (int i = 0; i < options.length; i++) { + crsOptions.addOption(options[i]); + } + for (Iterator iter = compilerOptions.iterator(); + iter.hasNext(); + ) { + crsOptions.addOption((Option) iter.next()); + } + // these are recognized but records with them are skipped + for (Iterator iter = ajc10Options.iterator(); + iter.hasNext(); + ) { + crsOptions.addOption((Option) iter.next()); + } + crsOptions.freeze(); + } + + Options getOptions() { + return crsOptions; + } + + /** + * @return unmodifiable Set of options sharing the + * family "compiler". + */ + Set compilerOptions() { + return compilerOptions; + } + + /** + * @param option the compiler Option to get name for + * @return null if option is null or not a compiler option, + * or the fully-qualified classname of the ICommand + * implementing the compiler. + */ + String compilerClassName(Option option) { + if ((null == option) + || (!compilerFamily.sameFamily(option.getFamily()))) { + return null; + } + return (String) compilerOptionToClassname.get(option); + } + + /** + * Check that the compiler class associated with a compiler + * option can be loaded. This check only happens once; + * the result is cached (by compilerOption key) for later calls. + * @param compilerOption the Option (family compiler) to check + * @return true if compiler class for this option can be loaded + */ + boolean compilerIsLoadable(Option compilerOption) { + LangUtil.throwIaxIfNull(compilerOption, "compilerName"); + synchronized (compilerOptionToLoadable) { + Boolean result = + (Boolean) compilerOptionToLoadable.get( + compilerOption); + if (null == result) { + MessageHandler sink = new MessageHandler(); + String compilerClassname = + (String) compilerOptionToClassname.get( + compilerOption); + if (null == compilerClassname) { + result = Boolean.FALSE; + } else { + ICommand c = + ReflectionFactory.makeCommand( + compilerClassname, + sink); + + if ((null == c) + || sink.hasAnyMessage( + IMessage.ERROR, + true)) { + result = Boolean.FALSE; + } else { + result = Boolean.TRUE; + } + } + compilerOptionToLoadable.put( + compilerOption, + result); + } + return result.booleanValue(); + } + } + } // CompilerRun.Spec.CRSOptions + } // CompilerRun.Spec +} // CompilerRun \ No newline at end of file diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/DirChanges.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/DirChanges.java new file mode 100644 index 000000000..13281607b --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/DirChanges.java @@ -0,0 +1,614 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +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; + +/** + * Calculate changes in a directory tree. + * This implements two different specification styles: + *
      + *
    • Specify files added, removed, updated, and/or a component + * to check any existing files
    • + *
    • Specify expected directory. When complete this checks that + * any files in expected directory are matched in the actual. + * (.class files are dissassembled before comparison.) + *
    • + *
    + * Usage: + *
      + *
    • Set up with any expected changes and/or an expected directory
    • + *
    • Set up with any file checker
    • + *
    • start(..) before changes. + * This issues messages for any removed files not found, + * which represent an error in the expected changes.
    • + *
    • Do whatever operations will change the directory
    • + *
    • 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
    • + *
    + * When comparing directories, this ignores any paths containing "CVS". + */ +public class DirChanges { + + public static final String DELAY_NAME = "dir-changes.delay"; + private static final long DELAY; + static { + long delay = 10l; + try { + delay = Long.getLong(DELAY_NAME).longValue(); + if ((delay > 40000) || (delay < 0)) { + delay = 10l; + } + } catch (Throwable t) { + // ignore + } + DELAY = delay; + } + + 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 + * specified in added/updated lists. + * This does not affect expected-directory comparison. + */ + 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 added file to exist", !EXISTS, spec.added, doCompare); + result &= exists("at start, expected unchanged file to exist", EXISTS, spec.unchanged, doCompare); + result &= exists("at start, expected updated file to exist", EXISTS, spec.updated, doCompare); + result &= exists("at start, expected removed file to exist", EXISTS, spec.removed, doCompare); + startTime = System.currentTimeMillis(); + // ensure tests don't complete in < 1 second, otherwise can confuse fast machines. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + 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. + * This sleeps before checking until at least DELAY milliseconds after start. + * @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 long targetTime = startTime + spec.delayInMilliseconds; + do { + long curTime = System.currentTimeMillis(); + if (curTime >= targetTime) { + break; + } + try { + Thread.sleep(targetTime-curTime); + } catch (InterruptedException e) { + break; + } + } while (true); + final IMessageHandler oldHandler = this.handler; + this.handler = handler; + try { + // variant 1: check specified files + // deferring comparison to end... + 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) { +// // XXX validate that unchanged mod-time did not change +// } + // variant 1: compare expected directory + if (result || !fastFail) { + result &= compareDir(srcBaseDir); + } + return result; + } finally { + this.handler = oldHandler; + baseDir = null; + startTime = 0l; + } + } + + /** + * 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.expDir) { + return true; + } + File expDir = new File(srcBaseDir, spec.expDir); + File actDir = baseDir; + //System.err.println("XXX comparing actDir=" + actDir + " expDir=" + expDir); + 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.expDir)) +// ? null +// : new File(baseDir, spec.expDir)); + 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); + } +// File-comparison code with a bit more generality -- too unweildy +// /** +// * 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. + * Currently the directory tree + * only is used to verify files that are expected + * and found after the process completes. + */ + public static class Spec implements IXmlWritable { + /** XML element name */ + public static final String XMLNAME = "dir-changes"; + + /** a symbolic name for the base directory */ + String dirToken; // XXX default to class? + + /** if set, then append to specified paths when seeking files */ + String defaultSuffix; + + /** relative path of dir with expected files for comparison */ + String expDir; + + long delayInMilliseconds = DELAY; + + /** if true, fail on first mis-match */ + boolean fastFail; + + /** relative paths (String) of expected files added */ + final ArrayList added; + + /** relative paths (String) of expected files removed/deleted */ + final ArrayList removed; + + /** relative paths (String) of expected files updated/changed */ + final ArrayList updated; + + /** relative paths (String) of expected files NOT + * added, removed, or changed + * XXX unchanged unimplemented + */ + final ArrayList unchanged; + + 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. + * @param expectedDirRelativePath path relative to the test base + * of the directory containing expected results for the output dir. + */ + public void setExpDir(String expectedDirRelativePath) { + expDir = expectedDirRelativePath; + } + + public void setDelay(String delay) { + if (null != delay) { + // let NumberFormatException propogate up + delayInMilliseconds = Long.parseLong(delay); + if (delayInMilliseconds < 0l) { + delayInMilliseconds = 0l; + } + } + } + + /** + * @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; + } + + /** @return true if some list was specified */ + private boolean hasFileList() { + return (!LangUtil.isEmpty(added) + || !LangUtil.isEmpty(removed) + || !LangUtil.isEmpty(updated) + || !LangUtil.isEmpty(unchanged) + ); + } + + /** + * Emit specification in XML form if not empty. + * This writes nothing if there is no expected dir + * and there are no added, removed, or changed. + * fastFail is written only if true, since the default is false. + */ + public void writeXml(XMLWriter out) { + if (!hasFileList() && LangUtil.isEmpty(expDir)) { + return; + } + // XXX need to permit defaults here... + out.startElement(XMLNAME, false); + if (!LangUtil.isEmpty(dirToken)) { + out.printAttribute("dirToken", dirToken.trim()); + } + if (!LangUtil.isEmpty(defaultSuffix)) { + out.printAttribute("defaultSuffix", defaultSuffix.trim()); + } + if (!LangUtil.isEmpty(expDir)) { + out.printAttribute("expDir", expDir.trim()); + } + 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)); + } + if (fastFail) { + out.printAttribute("fastFail", "true"); + } + 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 = iter.next(); + if (null == spec) { + continue; + } + spec.writeXml(out); + } + } + +} // class Spec + +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/FlatSuiteReader.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/FlatSuiteReader.java new file mode 100644 index 000000000..8d75d5620 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/FlatSuiteReader.java @@ -0,0 +1,376 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.aspectj.bridge.AbortException; +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.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.testing.util.UtilLineReader; +import org.aspectj.util.FileUtil; +import org.aspectj.util.LangUtil; + +/** + * 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(UtilLineReader) + */ + public Object make(final UtilLineReader 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, UtilLineReader 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 (String option: optionsCopy) { + 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.emptyList() : 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 = MessageUtil.fail("reading " + suiteFile, e); + throw new AbortException(m); + } + + return result; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/Globals.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/Globals.java new file mode 100644 index 000000000..ad62c9735 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/Globals.java @@ -0,0 +1,162 @@ +/* ******************************************************************* + * Copyright (c) 1999-2001 Xerox Corporation, + * 2002 Palo Alto Research Center, Incorporated (PARC) + * 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * Wes Isberg removed unused globals. + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.aspectj.util.FileUtil; +import org.aspectj.util.LangUtil; + +/** + */ +public class Globals { + + /** map from String version to String class implemented in that version or later */ + private static final Map VM_CLASSES; + + public static final String FORK_NAME = "harness.fork"; + // XXX in testing-drivers/../package.htm + /** 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"; + public static final String J2SE13_RTJAR_NAME = "j2se13.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; + // XXX used only by 1.0 compiler tests - deprecate? + public static final File J2SE13_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 File F_testingclient_jar = + new File(S_testingclient_jar); + public static final File F_aspectjrt_jar = new File(S_aspectjrt_jar); + public static final boolean globalsValid; + + static { + J2SE13_RTJAR = + getRtJarFor(J2SE13_RTJAR_NAME, "c:/home/apps/jdk13"); + J2SE14_RTJAR = + getRtJarFor(J2SE14_RTJAR_NAME, "c:/home/apps/jdk14"); + +// String forkSpec = getSystemProperty(FORK_NAME, null); + globalsValid = + (FileUtil.canReadFile(F_testingclient_jar) + && FileUtil.canReadFile(F_aspectjrt_jar) + && FileUtil.canReadFile(J2SE13_RTJAR) + && FileUtil.canReadFile(J2SE14_RTJAR)); + HashMap map = new HashMap(); + map.put("1.2", "java.lang.ref.Reference"); + map.put("1.3", "java.lang.reflect.Proxy"); + map.put("1.4", "java.nio.Buffer"); + map.put("1.5", "java.lang.annotation.Annotation"); + + VM_CLASSES = Collections.unmodifiableMap(map); + } + + private static File getRtJarFor( + String propertyName, + String defaultLocation) { + File j2seJar = null; + try { + String path = getSystemProperty(propertyName, defaultLocation); + File file = new File(path); + if (file.exists()) { + File rtjar = null; + if (path.endsWith("rt.jar")) { + rtjar = file; + } else if (file.isDirectory()) { + path = file.getAbsolutePath() + "/jre/lib/rt.jar"; + rtjar = new File(path); + } + if (rtjar.canRead() && rtjar.isFile()) { + j2seJar = rtjar; + } + } + } catch (Throwable t) { // avoid at all costs during static init + try { + t.printStackTrace(System.err); + } catch (Throwable x) { + // unable to log, better to just fail... + } + } + return j2seJar; + } + + /** + * + * @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. + */ + 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; + } + + + /** + * Detect whether Java version is supported. + * @param version String "1.2" or "1.3" or "1.4" + * @return true if the currently-running VM supports the version + * @throws IllegalArgumentException if version is not known + */ + public static final boolean supportsJava(String version) { + LangUtil.throwIaxIfNull(version, "version"); + String className = (String) VM_CLASSES.get(version); + if (null == className) { + throw new IllegalArgumentException("unknown version: " + version); + } + try { + Class.forName(className); + return true; + } catch (Throwable t) { + return false; + } + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/IAjcRun.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/IAjcRun.java new file mode 100644 index 000000000..e685df5dd --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/IAjcRun.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/harness/bridge/IRunSpec.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/IRunSpec.java new file mode 100644 index 000000000..dc35b493d --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/IRunSpec.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/harness/bridge/IncCompilerRun.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/IncCompilerRun.java new file mode 100644 index 000000000..7c7f2c54e --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/IncCompilerRun.java @@ -0,0 +1,503 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.aspectj.bridge.AbortException; +import org.aspectj.bridge.ICommand; +import org.aspectj.bridge.MessageUtil; +import org.aspectj.testing.ajde.CompileCommand; +import org.aspectj.testing.run.IRunIterator; +import org.aspectj.testing.run.IRunStatus; +import org.aspectj.testing.run.WrappedRunIterator; +import org.aspectj.testing.util.StructureModelUtil; +import org.aspectj.testing.util.StructureModelUtil.ModelIncorrectException; +import org.aspectj.testing.xml.AjcSpecXmlReader; +import org.aspectj.testing.xml.IXmlWritable; +import org.aspectj.testing.xml.SoftMessage; +import org.aspectj.testing.xml.XMLWriter; +import org.aspectj.util.FileUtil; +import org.aspectj.util.LangUtil; + +/** + * 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 + * (1) tag is "same", or + * (2) 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) { + if ("same".equals(spec.tag)) { + return true; + } + boolean result = false; + try { + 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 + // sigh - delay until after last last-mod-time + intHolder holder = new intHolder(); + List copied = new ArrayList(); + doStaging(validator,".java",holder,copied); + doStaging(validator,".jar",holder,copied); + doStaging(validator,".class",holder,copied); + doStaging(validator,".properties",holder,copied); // arbitrary resource extension + doStaging(validator,".xml",holder,copied); // arbitrary resource extension + if ((0 == holder.numCopies) && (0 == holder.numDeletes)) { + validator.fail("no files changed??"); + } else { + result = (0 == holder.numFails); + } + if (0 < copied.size()) { + File[] files = (File[]) copied.toArray(new File[0]); + FileUtil.sleepPastFinalModifiedTime(files); + } + } catch (NullPointerException npe) { + validator.fail("staging - input", npe); + } catch (IOException e) { + validator.fail("staging - operations", e); + } + return result; + } + + + private void doStaging(final Validator validator, final String toSuffix, + final intHolder holder,final List copied) + throws IOException + { + final String fromSuffix = "." + spec.tag + toSuffix; + final String clip = ".delete" + toSuffix; + FileFilter deleteOrCount = new FileFilter() { + /** 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++; + validator.info("copying file: " + path); + copied.add(file); + } else { + doCopy = false; + path = path.substring(0, path.length()-clip.length()) + toSuffix; + File toDelete = new File(path); + if (toDelete.delete()) { + validator.info("deleted file: " + path); + 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); + } + + private static class intHolder { + int numCopies; + int numDeletes; + int numFails; + } + + /** + * @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 + } + } + AjcMessageHandler handler = new AjcMessageHandler(spec.getMessages()); + boolean handlerResult = false; + boolean commandResult = false; + boolean result = false; + boolean report = false; + try { + handler.init(); + if (spec.fresh) { + if (compiler instanceof CompileCommand) { // urk + ((CompileCommand) compiler).buildNextFresh(); + } else { + MessageUtil.info(handler, "fresh not supported by compiler: " + compiler); + } + } +// final long startTime = System.currentTimeMillis(); + commandResult = compiler.repeatCommand(handler); + if (!spec.checkModel.equals("")) { + StructureModelUtil.checkModel(spec.checkModel); + } + // 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); + } + } + } catch (ModelIncorrectException e) { + MessageUtil.fail(status,e.getMessage()); + } 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 + } + + /** + * initializer/factory for IncCompilerRun. + */ + public static class Spec extends AbstractRunSpec { + public static final String XMLNAME = "inc-compile"; + + protected boolean fresh; + protected ArrayList classesAdded; + protected ArrayList classesRemoved; + protected ArrayList classesUpdated; + + protected String checkModel; + + /** + * skip description, skip sourceLocation, + * do keywords, skip options, do paths as classes, do comment, + * skip staging (always true), skip badInput (irrelevant) + * 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(); + checkModel=""; + } + + protected void initClone(Spec spec) + throws CloneNotSupportedException { + super.initClone(spec); + spec.fresh = fresh; + spec.tag = tag; + spec.classesAdded.clear(); + spec.classesAdded.addAll(classesAdded); + spec.classesRemoved.clear(); + spec.classesRemoved.addAll(classesRemoved); + spec.classesUpdated.clear(); + spec.classesUpdated.addAll(classesUpdated); + } + + public Object clone() throws CloneNotSupportedException { + Spec result = new Spec(); + initClone(result); + return result; + } + + + public void setFresh(boolean fresh) { + this.fresh = fresh; + } + + public void setTag(String input) { + tag = input; + } + + public void setCheckModel(String thingsToCheck) { + this.checkModel=thingsToCheck; + } + + public String toString() { + return "IncCompile.Spec(" + tag + ", " + super.toString() + ",["+checkModel+"])"; + } + + /** 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 = XMLWriter.makeAttribute("tag", tag); + out.startElement(xmlElementName, attr, false); + if (fresh) { + out.printAttribute("fresh", "true"); + } + super.writeAttributes(out); + out.endAttributes(); + if (!LangUtil.isEmpty(dirChanges)) { + DirChanges.Spec.writeXml(out, dirChanges); + } + SoftMessage.writeXml(out, getMessages()); + 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/test/java/org/aspectj/testing/harness/bridge/JavaRun.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/JavaRun.java new file mode 100644 index 000000000..aade35f48 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/JavaRun.java @@ -0,0 +1,899 @@ +/* ******************************************************************* + * 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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import org.aspectj.bridge.AbortException; +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.util.TestUtil; +import org.aspectj.testing.xml.SoftMessage; +import org.aspectj.testing.xml.XMLWriter; +import org.aspectj.util.FileUtil; +import org.aspectj.util.LangUtil; +import org.aspectj.weaver.loadtime.WeavingURLClassLoader; + +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.security.Permission; +import java.util.*; + +/** + * Run a class in this VM using reflection. + * Forked mode supported, but through system properties: + * - javarun.fork: anything to enable forking + * - javarun.java: path to java executable (optional) + * - javarun.java.home: JAVA_HOME for java (optional) + * (normally requires javarun.java) + * - javarun.classpath: a prefix to the run classpath (optional) + */ +public class JavaRun implements IAjcRun { + + private static void appendClasspath(StringBuffer cp, Object[] entries) { + if (!LangUtil.isEmpty(entries)) { + for (int i = 0; i < entries.length; i++) { + Object entry = entries[i]; + if (entry instanceof String) { + cp.append((String) entry); + cp.append(File.pathSeparator); + } else if (entry instanceof File) { + String s = FileUtil.getBestPath((File) entry); + if (null != s) { + cp.append(s); + cp.append(File.pathSeparator); + } + } + } + } + } + + 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, + * and, if fork is enabled, that java can be read. + * @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; + sandbox.javaRunInit(this); + 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, true), "classpath dirs") + && (!spec.forkSpec.fork + || validator.canRead(spec.forkSpec.java, "java")) + ); + + } + + /** caller must record any exceptions */ + public boolean run(IRunStatus status) + throws IllegalAccessException, + InvocationTargetException, + ClassNotFoundException, + NoSuchMethodException { + boolean completedNormally = false; + boolean passed = false; + 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); + boolean includeClassesDir = true; + File[] dirs = sandbox.getClasspathDirectories(readable, this, includeClassesDir); + completedNormally = (spec.forkSpec.fork) + ? runInOtherVM(status, libs, dirs) + : runInSameVM(status, libs, dirs); + passed = completedNormally; + } finally { + if (!passed || !status.runResult()) { + MessageUtil.info(status, spec.toLongString()); + MessageUtil.info(status, "sandbox: " + sandbox); + } + } + return passed; + } + protected boolean runInSameVM(IRunStatus status, File[] libs, File[] dirs) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { + ClassLoader loader = null; + boolean completedNormally = false; + boolean passed = false; + ByteArrayOutputStream outSnoop = null; + PrintStream oldOut = null; + ByteArrayOutputStream errSnoop = null; + PrintStream oldErr = null; + if (spec.outStreamIsError) { + outSnoop = new ByteArrayOutputStream(); + oldOut = System.out; + System.setOut(new PrintStream(outSnoop, true)); + } + if (spec.errStreamIsError) { + errSnoop = new ByteArrayOutputStream(); + oldErr = System.err; + System.setErr(new PrintStream(errSnoop, true)); + } + Class targetClass = null; + try { + final URL[] clAndLibs; + { + File[] files = sandbox.findFiles(spec.classpath); + URL[] clURLs = FileUtil.getFileURLs(files); + URL[] libURLs = FileUtil.getFileURLs(libs); + clAndLibs = new URL[clURLs.length + libURLs.length]; + System.arraycopy(clURLs, 0, clAndLibs , 0, clURLs.length); + System.arraycopy(libURLs, 0, clAndLibs, clURLs.length, libURLs.length); + } + if (!spec.isLTW()) { + loader = new TestClassLoader(clAndLibs, dirs); + } else { + final URL[] aspectURLs; + { + File[] files = sandbox.findFiles(spec.aspectpath); + aspectURLs = FileUtil.getFileURLs(files); + } + ArrayList classpath = new ArrayList(); + classpath.addAll(Arrays.asList(aspectURLs)); + final URL[] classURLs; + { + classpath.addAll(Arrays.asList(clAndLibs)); + URL[] urls = FileUtil.getFileURLs(dirs); + classpath.addAll(Arrays.asList(urls)); + classpath.add(FileUtil.getFileURL(Globals.F_aspectjrt_jar)); + classpath.add(FileUtil.getFileURL(Globals.F_testingclient_jar)); + classURLs = (URL[]) classpath.toArray(new URL[0]); + } + + ClassLoader parent = JavaRun.class.getClassLoader(); + loader = new WeavingURLClassLoader(classURLs, aspectURLs, parent); + } + // 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); + RunSecurityManager.ME.setJavaRunThread(this); + main.invoke(null, new Object[] { spec.getOptionsArray() }); + completedNormally = true; + boolean snoopFailure = + ((null != errSnoop) && 0 < errSnoop.size()) + || ((null != outSnoop) && 0 < outSnoop.size()); + passed = !snoopFailure && (null == spec.expectedException); + } catch (AbortException e) { + if (expectedException(e)) { + passed = true; + } else { + throw e; + } + } catch (InvocationTargetException e) { + // this and following clauses catch ExitCalledException + Throwable thrown = LangUtil.unwrapException(e); + if (null == thrown) { + throw e; + } + if (thrown instanceof RunSecurityManager.ExitCalledException) { + int i = ((RunSecurityManager.ExitCalledException) thrown).exitCode; + status.finish(new Integer(i)); + } else if (thrown instanceof RunSecurityManager.AwtUsedException) { + MessageUtil.fail(status, "test code should not use the AWT event queue"); + throw (RunSecurityManager.AwtUsedException) thrown; + // same as: status.thrown(thrown); + } else if (expectedException(thrown)) { + passed = true; + } else if (thrown instanceof RuntimeException) { + throw (RuntimeException) thrown; + } else if (thrown instanceof Error) { + throw (Error) thrown; + } else { + throw e; + } + } catch (RunSecurityManager.ExitCalledException e) { + // XXX need to update run validator (a) to accept null result or (b) to require zero result, and set 0 if completed normally + status.finish(new Integer(e.exitCode)); + } catch (ClassNotFoundException e) { + String[] classes = FileUtil.listFiles(sandbox.classesDir); + MessageUtil.info(status, "sandbox.classes: " + Arrays.asList(classes)); + MessageUtil.fail(status, null, e); + } finally { + if (null != oldOut) { + System.setOut(oldOut); + } + if (null != oldErr) { + System.setErr(oldErr); + } + RunSecurityManager.ME.releaseJavaRunThread(this); + if (!completedNormally) { + MessageUtil.info(status, "targetClass: " + targetClass); + MessageUtil.info(status, "loader: " + loader); + } + } + return passed; + } + + /** + * Run in another VM by grabbing Java, bootclasspath, classpath, etc. + * This assumes any exception or output to System.err is a failure, + * and any normal completion is a pass. + * @param status + * @param libs + * @param dirs + * @return + */ + protected boolean runInOtherVM(IRunStatus status, File[] libs, File[] dirs) { + // assert spec.fork || !LangUtil.isEmpty(spec.aspectpath); + ArrayList cmd = new ArrayList(); + cmd.add(FileUtil.getBestPath(spec.forkSpec.java)); + if (!LangUtil.isEmpty(spec.forkSpec.vmargs)) { + cmd.addAll(Arrays.asList(spec.forkSpec.vmargs)); + } + final String classpath; + { + StringBuffer cp = new StringBuffer(); + appendClasspath(cp, spec.forkSpec.bootclasspath); + appendClasspath(cp, dirs); + appendClasspath(cp, libs); + File[] classpathFiles = sandbox.findFiles(spec.classpath); + int cpLength = (null == classpathFiles ? 0 : classpathFiles.length); + int spLength = (null == spec.classpath ? 0 : spec.classpath.length); + if (cpLength != spLength) { + throw new Error("unable to find " + Arrays.asList(spec.classpath) + + " got " + Arrays.asList(classpathFiles)); + } + appendClasspath(cp, classpathFiles); + File[] stdlibs = {Globals.F_aspectjrt_jar, Globals.F_testingclient_jar}; + appendClasspath(cp, stdlibs); + classpath = cp.toString(); + } + if (!spec.isLTW()) { + cmd.add("-classpath"); + cmd.add(classpath); + } else { + // verify 1.4 or above, assuming same vm as running this + if (!Globals.supportsJava("1.4")) { + throw new Error("load-time weaving test requires Java 1.4+"); + } + cmd.add("-Djava.system.class.loader=org.aspectj.weaver.WeavingURLClassLoader"); + // assume harness VM classpath has WeavingURLClassLoader (but not others) + cmd.add("-classpath"); + cmd.add(System.getProperty("java.class.path")); + + File[] aspectJars = sandbox.findFiles(spec.aspectpath); + if (aspectJars.length != spec.aspectpath.length) { + throw new Error("unable to find " + Arrays.asList(spec.aspectpath)); + } + StringBuffer cp = new StringBuffer(); + appendClasspath(cp, aspectJars); + cmd.add("-Daj.aspect.path=" + cp.toString()); + cp.append(classpath); // appendClasspath writes trailing delimiter + cmd.add("-Daj.class.path=" + cp.toString()); + } + cmd.add(spec.className); + cmd.addAll(spec.options); + String[] command = (String[]) cmd.toArray(new String[0]); + + final IMessageHandler handler = status; + // setup to run asynchronously, pipe streams through, and report errors + class DoneFlag { + boolean done; + boolean failed; + int code; + } + final StringBuffer commandLabel = new StringBuffer(); + final DoneFlag doneFlag = new DoneFlag(); + LangUtil.ProcessController controller + = new LangUtil.ProcessController() { + protected void doCompleting(Thrown ex, int result) { + if (!ex.thrown && (0 == result)) { + doneFlag.done = true; + return; // no errors + } + // handle errors + String context = spec.className + + " command \"" + + commandLabel + + "\""; + if (null != ex.fromProcess) { + if (!expectedException(ex.fromProcess)) { + String m = "Exception running " + context; + MessageUtil.abort(handler, m, ex.fromProcess); + doneFlag.failed = true; + } + } else if (0 != result) { + doneFlag.code = result; + } + if (null != ex.fromInPipe) { + String m = "Error processing input pipe for " + context; + MessageUtil.abort(handler, m, ex.fromInPipe); + doneFlag.failed = true; + } + if (null != ex.fromOutPipe) { + String m = "Error processing output pipe for " + context; + MessageUtil.abort(handler, m, ex.fromOutPipe); + doneFlag.failed = true; + } + if (null != ex.fromErrPipe) { + String m = "Error processing error pipe for " + context; + MessageUtil.abort(handler, m, ex.fromErrPipe); + doneFlag.failed = true; + } + doneFlag.done = true; + } + }; + controller.init(command, spec.className); + if (null != spec.forkSpec.javaHome) { + controller.setEnvp(new String[] {"JAVA_HOME=" + spec.forkSpec.javaHome}); + } + commandLabel.append(Arrays.asList(controller.getCommand()).toString()); + final ByteArrayOutputStream errSnoop + = new ByteArrayOutputStream(); + final ByteArrayOutputStream outSnoop + = new ByteArrayOutputStream(); + controller.setErrSnoop(errSnoop); + controller.setOutSnoop(outSnoop); + controller.start(); + // give it 3 minutes... + long maxTime = System.currentTimeMillis() + 3 * 60 * 1000; + boolean waitingForStop = false; + while (!doneFlag.done) { + if (maxTime < System.currentTimeMillis()) { + if (waitingForStop) { // hit second timeout - bail + break; + } + MessageUtil.fail(status, "timeout waiting for process"); + doneFlag.failed = true; + controller.stop(); + // wait 1 minute to evaluate results of stopping + waitingForStop = true; + maxTime = System.currentTimeMillis() + 1 * 60 * 1000; + } + try { + Thread.sleep(300); + } catch (InterruptedException e) { + // ignore + } + } + + boolean foundException = false; + if (0 < errSnoop.size()) { + if (expectedException(errSnoop)) { + foundException = true; + } else if (spec.errStreamIsError) { + MessageUtil.error(handler, errSnoop.toString()); + if (!doneFlag.failed) { + doneFlag.failed = true; + } + } else { + MessageUtil.info(handler, "Error stream: " + errSnoop.toString()); + } + } + if (0 < outSnoop.size()) { + if (expectedException(outSnoop)) { + foundException = true; + } else if (spec.outStreamIsError) { + MessageUtil.error(handler, outSnoop.toString()); + if (!doneFlag.failed) { + doneFlag.failed = true; + } + } else { + MessageUtil.info(handler, "Output stream: " + outSnoop.toString()); + } + } + if (!foundException) { + if (null != spec.expectedException) { + String m = " expected exception " + spec.expectedException; + MessageUtil.fail(handler, m); + doneFlag.failed = true; + } else if (0 != doneFlag.code) { + String m = doneFlag.code + " result from " + commandLabel; + MessageUtil.fail(handler, m); + doneFlag.failed = true; + } + } + if (doneFlag.failed) { + MessageUtil.info(handler, "other-vm command-line: " + commandLabel); + } + return !doneFlag.failed; + } + + protected boolean expectedException(Throwable thrown) { + if (null != spec.expectedException) { + String cname = thrown.getClass().getName(); + if (-1 != cname.indexOf(spec.expectedException)) { + return true; // caller sets value for returns normally + } + } + return false; + } + + protected boolean expectedException(ByteArrayOutputStream bout) { + return ((null != spec.expectedException) + && (-1 != bout.toString().indexOf(spec.expectedException))); + } + + /** + * 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 + ")"; + } + + /** + * Struct class for fork attributes and initialization. + * This supports defaults for forking using system properties + * which will be overridden by any specification. + * (It differs from CompilerRun, which supports option + * overriding by passing values as harness arguments.) + */ + public static class ForkSpec { + /** + * key for system property for default value for forking + * (true if set to true) + */ + public static String FORK_KEY = "javarun.fork"; + public static String JAVA_KEY = "javarun.java"; + public static String VM_ARGS_KEY = "javarun.vmargs"; + public static String JAVA_HOME_KEY = "javarun.java.home"; + public static String BOOTCLASSPATH_KEY = "javarun.bootclasspath"; + static final ForkSpec FORK; + static { + ForkSpec fork = new ForkSpec(); + fork.fork = Boolean.getBoolean(FORK_KEY); + fork.java = getFile(JAVA_KEY); + if (null == fork.java) { + fork.java = LangUtil.getJavaExecutable(); + } + fork.javaHome = getFile(JAVA_HOME_KEY); + fork.bootclasspath = XMLWriter.unflattenList(getProperty(BOOTCLASSPATH_KEY)); + fork.vmargs = XMLWriter.unflattenList(getProperty(VM_ARGS_KEY)); + FORK = fork; + } + private static File getFile(String key) { + String path = getProperty(key); + if (null != path) { + File result = new File(path); + if (result.exists()) { + return result; + } + } + return null; + } + private static String getProperty(String key) { + try { + return System.getProperty(key); + } catch (Throwable t) { + return null; + } + } + private boolean fork; + private String[] bootclasspath; + private File java; + private File javaHome; + private String[] vmargs; + + private ForkSpec() { + copy(FORK); + } + + private void copy(ForkSpec forkSpec) { + if (null != forkSpec) { + fork = forkSpec.fork; + bootclasspath = forkSpec.bootclasspath; + java = forkSpec.java; + javaHome = forkSpec.javaHome; + vmargs = forkSpec.vmargs; + } + } + + /** + * @return "" or bootclasspath with File.pathSeparator internal delimiters + */ + String getBootclasspath() { + if (LangUtil.isEmpty(bootclasspath)) { + return ""; + } + return FileUtil.flatten(bootclasspath, null); + } + } + + /** + * Initializer/factory for JavaRun. + * The classpath is not here but precalculated in the Sandbox. + */ + public static class Spec extends AbstractRunSpec { + static { + try { + System.setSecurityManager(RunSecurityManager.ME); + } catch (Throwable t) { + System.err.println("JavaRun: Security manager set - no System.exit() protection"); + } + } + public static final String XMLNAME = "run"; + /** + * skip description, skip sourceLocation, + * do keywords, do options, skip paths, do comment, + * skip staging, skip badInput, + * 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; + + /** Alternative to classname for specifying what to run modulename/type */ + protected String module; + + /** minimum required version of Java, if any */ + protected String javaVersion; + + /** if true, skip Tester setup (e.g., if Tester n/a) */ + protected boolean skipTester; + + /** if true, report text to output stream as error */ + protected boolean outStreamIsError; + + /** if true, report text to error stream as error */ + protected boolean errStreamIsError = true; + + protected final ForkSpec forkSpec; + protected String[] aspectpath; + protected boolean useLTW; + protected String[] classpath; + protected String expectedException; + + public Spec() { + super(XMLNAME); + setXMLNames(NAMES); + forkSpec = new ForkSpec(); + } + + protected void initClone(Spec spec) + throws CloneNotSupportedException { + super.initClone(spec); + spec.className = className; + spec.errStreamIsError = errStreamIsError; + spec.javaVersion = javaVersion; + spec.outStreamIsError = outStreamIsError; + spec.skipTester = skipTester; + spec.forkSpec.copy(forkSpec); + } + + public Object clone() throws CloneNotSupportedException { + Spec result = new Spec(); + initClone(result); + return result; + } + + public boolean isLTW() { + return useLTW || (null != aspectpath); + } + + /** + * @param version "1.1", "1.2", "1.3", "1.4" + * @throws IllegalArgumentException if version is not recognized + */ + public void setJavaVersion(String version) { + Globals.supportsJava(version); + this.javaVersion = version; + } + + /** @className fully-qualified name of the class to run */ + public void setClassName(String className) { + this.className = className; + } + + public void setModule(String module) { + this.module = module; + } + + public void setLTW(String ltw) { + useLTW = TestUtil.parseBoolean(ltw); + } + + public void setAspectpath(String path) { + this.aspectpath = XMLWriter.unflattenList(path); + } + public void setException(String exception) { + this.expectedException = exception; + } + + public void setClasspath(String path) { + this.classpath = XMLWriter.unflattenList(path); + } + public void setErrStreamIsError(String errStreamIsError) { + this.errStreamIsError = TestUtil.parseBoolean(errStreamIsError); + } + + public void setOutStreamIsError(String outStreamIsError) { + this.outStreamIsError = TestUtil.parseBoolean(outStreamIsError); + } + + /** @param skip if true, then do not set up Tester */ + public void setSkipTester(boolean skip) { + skipTester = skip; + } + + public void setFork(boolean fork) { + forkSpec.fork = fork; + } + + /** + * @param vmargs comma-delimited list of arguments for java, + * typically -Dname=value,-DanotherName="another value" + */ + public void setVmArgs(String vmargs) { + forkSpec.vmargs = XMLWriter.unflattenList(vmargs); + } + + /** 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 = XMLWriter.makeAttribute("class", className); + out.startElement(xmlElementName, attr, false); + if (skipTester) { + out.printAttribute("skipTester", "true"); + } + if (null != javaVersion) { + out.printAttribute("vm", javaVersion); + } + if (outStreamIsError) { + out.printAttribute("outStreamIsError", "true"); + } + if (!errStreamIsError) { // defaults to true + out.printAttribute("errStreamIsError", "false"); + } + super.writeAttributes(out); + out.endAttributes(); + if (!LangUtil.isEmpty(dirChanges)) { + DirChanges.Spec.writeXml(out, dirChanges); + } + SoftMessage.writeXml(out, getMessages()); + 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: + *
      + *
    • current VM is not at least any specified javaVersion
    • + *
    + * @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) && (!Globals.supportsJava(javaVersion))) { + skipMessage(handler, "requires Java version " + javaVersion); + return false; + } + return true; + } + } + /** + * This permits everything but System.exit() in the context of a + * thread set by JavaRun. + * XXX need to update for thread spawned by that thread + * XXX need to update for awt thread use after AJDE wrapper doesn't + */ + public static class RunSecurityManager extends SecurityManager { + public static RunSecurityManager ME = new RunSecurityManager(); + private Thread runThread; + private RunSecurityManager(){} + private synchronized void setJavaRunThread(JavaRun run) { + LangUtil.throwIaxIfNull(run, "run"); + runThread = Thread.currentThread(); + } + private synchronized void releaseJavaRunThread(JavaRun run) { + LangUtil.throwIaxIfNull(run, "run"); + runThread = null; + } + /** @throws ExitCalledException if called from the JavaRun-set thread */ + public void checkExit(int exitCode) throws ExitCalledException { + if ((null != runThread) && runThread.equals(Thread.currentThread())) { + throw new ExitCalledException(exitCode); + } + } + public void checkAwtEventQueueAccess() { + if ((null != runThread) && runThread.equals(Thread.currentThread())) { + throw new AwtUsedException(); + } + } + public void checkSystemClipboardAccess() { + // permit + } + // used by constrained calls + public static class ExitCalledException extends SecurityException { + public final int exitCode; + public ExitCalledException(int exitCode) { + this.exitCode = exitCode; + } + } + public static class AwtUsedException extends SecurityException { + public AwtUsedException() { } + } + // permit everything else + public void checkAccept(String arg0, int arg1) { + } + public void checkAccess(Thread arg0) { + } + public void checkAccess(ThreadGroup arg0) { + } + public void checkConnect(String arg0, int arg1) { + } + public void checkConnect(String arg0, int arg1, Object arg2) { + } + public void checkCreateClassLoader() { + } + public void checkDelete(String arg0) { + } + public void checkExec(String arg0) { + } + public void checkLink(String arg0) { + } + public void checkListen(int arg0) { + } + public void checkMemberAccess(Class arg0, int arg1) { + } + public void checkMulticast(InetAddress arg0) { + } + public void checkMulticast(InetAddress arg0, byte arg1) { + } + public void checkPackageAccess(String arg0) { + } + public void checkPackageDefinition(String arg0) { + } + public void checkPermission(Permission arg0) { + } + public void checkPermission(Permission arg0, Object arg1) { + } + public void checkPrintJobAccess() { + } + public void checkPropertiesAccess() { + } + public void checkPropertyAccess(String arg0) { + } + public void checkRead(FileDescriptor arg0) { + } + public void checkRead(String arg0) { + } + public void checkRead(String arg0, Object arg1) { + } + public void checkSecurityAccess(String arg0) { + } + public void checkSetFactory() { + } + public boolean checkTopLevelWindow(Object arg0) { + return true; + } + public void checkWrite(FileDescriptor arg0) { + } + public void checkWrite(String arg0) { + } + + } + +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/RunSpecIterator.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/RunSpecIterator.java new file mode 100644 index 000000000..e5792236b --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/RunSpecIterator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.util.ArrayList; + +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; + + +/** + * 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/test/java/org/aspectj/testing/harness/bridge/Sandbox.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/Sandbox.java new file mode 100644 index 000000000..f7f4df31e --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/Sandbox.java @@ -0,0 +1,575 @@ +/* ******************************************************************* + * 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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.Arrays; + +import org.aspectj.bridge.ICommand; +import org.aspectj.bridge.IMessage; +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.testing.taskdefs.AjcTaskCompileCommand; +import org.aspectj.testing.util.Diffs; +import org.aspectj.util.FileUtil; +import org.aspectj.util.LangUtil; + +/** + * 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). + *

    + * 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: + *

  • Both have an extra (typed) "caller" parameter which must not + * be null, authenticating that the caller is known & valid.
  • + *
  • A getter throws IllegalStateException if called before the setter
  • + *
  • A setter throws IllegalStateException if called after the getter
  • + * 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[] compileClasspath; + + private String bootClasspath; + + /** aspectpath entries, set by CompileRun.setup(..) */ + private File[] aspectpath; + + /** 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; + + private transient boolean compilerRunInit; + + /** @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; + { + File baseDir = FileUtil.getTempDir("Sandbox"); + if (!baseDir.isAbsolute()) { + baseDir = baseDir.getAbsoluteFile(); + } + sandboxDir = baseDir; + } + 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) { // need to add ICommand.quit() + if (command instanceof AjcTaskCompileCommand) { // XXX urk! + ((AjcTaskCompileCommand) command).quit(); + } + command = null; + } + // also try to clear sandbox/filesystem. + // If locked by suite, we can't. + if (null != validator) { + validator.deleteTempFiles(true); + } + } + +// /** +// * 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(CompilerRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + assertState(null != command, "command never set"); + return command; + } + + 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; + } + + /** + * Get the files with names (case-sensitive) + * under the staging or test base directories. + * @param names + * @return + */ + File[] findFiles(final String[] names) { + ArrayList result = new ArrayList(); + NamesFilter filter = new NamesFilter(names); + File[] bases = { testBaseDir, sandboxDir }; + for (int i = 0; i < bases.length; i++) { + File base = bases[i]; + if ((null == base) || !base.canRead()) { + continue; + } + result.addAll(Arrays.asList(FileUtil.listFiles(base, filter))); + } + return (File[]) result.toArray(new File[0]); + } + File getTestBaseSrcDir(JavaRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + return testBaseSrcDir; + } + + void defaultTestBaseSrcDir(JavaRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + if (null != testBaseSrcDir) { + throw new IllegalStateException("testBaseSrcDir not null"); + } + testBaseSrcDir = testBaseDir; + } + + static boolean readableDir(File dir) { + return ((null != dir) && dir.isDirectory() && dir.canRead()); + } + + void compilerRunInit(CompilerRun caller, File testBaseSrcDir, + File[] aspectPath, boolean aspectpathReadable, + File[] classPath, boolean classpathReadable, + String bootclassPath + ) { + if (null != testBaseSrcDir) { + setTestBaseSrcDir(testBaseSrcDir, caller); + } + if ((null != aspectPath) && (0 < aspectPath.length)) { + setAspectpath(aspectPath, aspectpathReadable, caller); + } + if ((null != classPath) && (0 < classPath.length)) { + setClasspath(classPath, classpathReadable, caller); + } + + setBootclasspath(bootclassPath, caller); + compilerRunInit = true; + } + void javaRunInit(JavaRun caller) { + if (!compilerRunInit) { + testBaseSrcDir = testBaseDir; + // default to aspectjrt.jar? + compileClasspath = new File[0]; + + } + } + + /** @throws IllegalArgumentException unless a readable directory */ + private 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; + } + + /** + * Set aspectpath. + * @param readable if true, then throw IllegalArgumentException if not readable + */ + private void setAspectpath(File[] files, boolean readable, CompilerRun caller) { + LangUtil.throwIaxIfNull(files, "files"); + LangUtil.throwIaxIfNull(caller, "caller"); + assertState(null == aspectpath, "aspectpath already written"); + aspectpath = 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 aspectpath entry: " + files[i]); + } + aspectpath[i] = files[i]; + } + } + + /** + * Set bootclasspath, presumed to be delimited by + * File.pathSeparator and have valid entries. + * @param bootClasspath + * @param caller + */ + private void setBootclasspath(String bootClasspath, CompilerRun caller) { + this.bootClasspath = bootClasspath; + } + + /** + * Set compile classpath. + * @param readable if true, then throw IllegalArgumentException if not readable + */ + private void setClasspath(File[] files, boolean readable, CompilerRun caller) { + LangUtil.throwIaxIfNull(files, "files"); + LangUtil.throwIaxIfNull(caller, "caller"); + assertState(!gotClasspath, "classpath already read"); + compileClasspath = 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]); + } + compileClasspath[i] = files[i]; + } + } + +// /** +// * Get run classpath +// * @param caller unused except to restrict usage to non-null JavaRun. +// * @throws IllegalStateException if compileClasspath was not set. +// * @throws IllegalArgumentException if caller is null +// */ +// File[] getRunClasspath(JavaRun caller) { +// LangUtil.throwIaxIfNull(caller, "caller"); +// assertState(null != compileClasspath, "classpath not set"); +// int compilePathLength = compileClasspath.length; +// int aspectPathLength = (null == aspectpath ? 0 : aspectpath.length); +// File[] result = new File[aspectPathLength + compilePathLength]; +// System.arraycopy(compileClasspath, 0, result, 0, compilePathLength); +// if (0 < aspectPathLength) { +// System.arraycopy(aspectpath, 0, result, compilePathLength, aspectPathLength); +// } +// return result; +// } + + /** + * Get directories for the run classpath by selecting them + * from the compile classpath. + * This ignores aspectpath since it may contain only jar files. + * @param readable if true, omit non-readable directories + */ + File[] getClasspathDirectories( + boolean readable, + JavaRun caller, + boolean includeOutput) { + LangUtil.throwIaxIfNull(caller, "caller"); + assertState(null != compileClasspath, "classpath not set"); + ArrayList result = new ArrayList(); + File[] src = compileClasspath; + for (int i = 0; i < src.length; i++) { + File f = src[i]; + if ((null != f) && (f.isDirectory()) && (!readable || f.canRead())) { + result.add(f); + } + } + if (includeOutput && (null != classesDir) + && (!readable || classesDir.canRead())) { + result.add(classesDir); + } + return (File[]) result.toArray(new File[0]); + } + + /** + * Get the jars belonging on the run classpath, including classpath + * and aspectpath entries. + * @param readable if true, omit non-readable directories + */ + File[] getClasspathJars(boolean readable, JavaRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + assertState(null != compileClasspath, "classpath not set"); + ArrayList result = new ArrayList(); + File[][] src = new File[][] { compileClasspath, aspectpath }; + for (int i = 0; i < src.length; i++) { + File[] paths = src[i]; + int len = (null == paths ? 0 : paths.length); + for (int j = 0; j < len; j++) { + File f = paths[j]; + if (FileUtil.isZipFile(f) && (!readable || f.canRead())) { + result.add(f); + } + } + } + return (File[]) result.toArray(new File[0]); + } + + /** + * Get the list of aspect jars as a String. + * @return String of classpath entries delimited internally by File.pathSeparator + */ + String aspectpathToString(CompilerRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + return FileUtil.flatten(aspectpath, File.pathSeparator); + } + + /** + * Get the compile classpath as a String. + * @return String of classpath entries delimited internally by File.pathSeparator + */ + String classpathToString(CompilerRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + return FileUtil.flatten(compileClasspath, File.pathSeparator); + } + + /** + * Get the bootClasspath as a String. + * @return String of bootclasspath entries delimited internally by File.pathSeparator + */ + String getBootclasspath(CompilerRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + return bootClasspath; + } + + /** + * Get the bootClasspath as a String. + * @return String of bootclasspath entries delimited internally by File.pathSeparator + */ + String getBootclasspath(JavaRun caller) { + LangUtil.throwIaxIfNull(caller, "caller"); + return bootClasspath; + } + private static class NamesFilter implements FileFilter { + private final String[] names; + private NamesFilter(String[] names) { + this.names = names; + } + public boolean accept(File file) { + if (null != file) { + String name = file.getName(); + if ((null != name) && (null != names)) { + for (int i = 0; i < names.length; i++) { + if (name.equals(names[i])) { + return true; + } + } + } + } + return false; + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/TestingBridgeTests.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/TestingBridgeTests.java deleted file mode 100644 index 1fd4cb342..000000000 --- a/testing/src/test/java/org/aspectj/testing/harness/bridge/TestingBridgeTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* ******************************************************************* - * 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 v1.0 - * which accompanies this distribution and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Xerox/PARC initial implementation - * ******************************************************************/ - - -package org.aspectj.testing.harness.bridge; - -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; - -public class TestingBridgeTests extends TestCase { - - public static Test suite() { - TestSuite suite = new TestSuite(TestingBridgeTests.class.getName()); - // XXX disabled DirChangesTest pending implementation - //$JUnit-BEGIN$ - suite.addTestSuite(AbstractRunSpecTest.class); - suite.addTestSuite(AjcSpecTest.class); - suite.addTestSuite(CompilerRunTest.class); - suite.addTestSuite(CompilerRunSpecTest.class); - suite.addTestSuite(ParseTestCase.class); - //$JUnit-END$ - return suite; - } - - public TestingBridgeTests(String name) { super(name); } - -} diff --git a/testing/src/test/java/org/aspectj/testing/harness/bridge/Validator.java b/testing/src/test/java/org/aspectj/testing/harness/bridge/Validator.java new file mode 100644 index 000000000..37ddd005e --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/harness/bridge/Validator.java @@ -0,0 +1,564 @@ +/* ******************************************************************* + * 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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.harness.bridge; + +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; + +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; + +/** + * 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: + *
  • Toggle abortOnException to throw AbortException on failure
  • + *
  • push their own message handler to redirect messages + * (pop to remove if any pushed)
  • + *
  • subclass this to reimplement fail(String) + *

    + * 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 + *

  • {null check} array
  • + *
  • {null check} {message}[#}
  • + */ + 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 + *
  • {null check} list
  • + *
  • {null check} {message}[#}
  • + */ + 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 + *
  • {null check} dir array
  • + *
  • "#: not a File {file}"
  • + *
  • {canRead} {message}[#}
  • + */ + 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 + *
  • {null check} files
  • + *
  • "#: not a File {file}"
  • + *
  • {canRead} {message}[#}
  • + */ + 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 + *
  • {null check} dir array
  • + *
  • "#: not a File {file}"
  • + *
  • {canReadDir} {message}[#}
  • + */ + 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 + *
  • {null check} dirs
  • + *
  • "#: not a File {file}"
  • + *
  • {canReadDir} {message}[#}
  • + */ + 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 + *
  • {null check} dir array
  • + *
  • "#: not a File {file}"
  • + *
  • {canWrite} {message}[#}
  • + */ + 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 + *
  • {null check} files
  • + *
  • "#: not a File {file}"
  • + *
  • {canWrite} {message}[#}
  • + */ + 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 + *
  • {null check} dir array
  • + *
  • "#: not a File {file}"
  • + *
  • {canWriteDir} {message}[#}
  • + */ + 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 + *
  • {null check} dirs
  • + *
  • "#: not a File {file}"
  • + *
  • {canWriteDir} {message}[#}
  • + */ + 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 = ""; + } + 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 deleteTempDirs(), + * 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 { + 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();) { + if (deleteFile((File) iter.next(), reportFailures)) { + iter.remove(); + } + } + for (ListIterator iter = sandboxes.listIterator(); iter.hasNext();) { + Sandbox sandbox = (Sandbox) iter.next(); + // XXX assumes all dirs are in sandboxDir + if (deleteFile(sandbox.sandboxDir, reportFailures)) { + iter.remove(); + } + } + } + } + + /** + * 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 boolean deleteFile(File file, boolean reportFailures) { + if (null == file) { + if (reportFailures) { + fail("unable to delete null file"); + } + return true; // null file - skip + } + FileUtil.deleteContents(file); + if (file.exists()) { + file.delete(); + } + if (!file.exists()) { + return true; + } else if (reportFailures) { + fail("unable to delete " + file); + } + return false; + } + + /** @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/test/java/org/aspectj/testing/run/IRun.java b/testing/src/test/java/org/aspectj/testing/run/IRun.java new file mode 100644 index 000000000..57639016b --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/IRun.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/run/IRunIterator.java b/testing/src/test/java/org/aspectj/testing/run/IRunIterator.java new file mode 100644 index 000000000..5035b216f --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/IRunIterator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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 + * iterationCompleted() + */ + 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 + * iterationCompleted() + * @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 + * iterationCompleted() + */ + boolean abortOnFailure(); // XXX supply IRun or IRunStatus? + + /** called when hasNextRun() and nextRun() will no longer be called */ + void iterationCompleted(); +} diff --git a/testing/src/test/java/org/aspectj/testing/run/IRunListener.java b/testing/src/test/java/org/aspectj/testing/run/IRunListener.java new file mode 100644 index 000000000..4318d618f --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/IRunListener.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/run/IRunStatus.java b/testing/src/test/java/org/aspectj/testing/run/IRunStatus.java new file mode 100644 index 000000000..9c8665728 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/IRunStatus.java @@ -0,0 +1,195 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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 + /** + * Detect whether a message of a given kind has been handled. + * @param kind the IMessage.Kind of the message to detect + * @param orGreater if true, then also accept any message of a greater kind + * @param includeChildren if true, then also search in any child IRunStatus + * @return true if any such message is detected + */ + boolean hasAnyMessage(IMessage.Kind kind, boolean orGreater, boolean includeChildren); + + /** + * Get all messages or those of a specific kind, optionally in children as well + * Pass null to get all kinds. + * @param kind the IMessage.Kind expected, or null for all messages + * @param orGreater if true, also get any greater than the target kind + * as determined by IMessage.Kind.COMPARATOR + * @param includeChildren if true, then also search in any child IRunStatus + * @return IMessage[] of messages of the right kind, or IMessage.NONE + */ + IMessage[] getMessages(IMessage.Kind kind, boolean orGreater, boolean includeChildren); + + /** + * 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/test/java/org/aspectj/testing/run/IRunValidator.java b/testing/src/test/java/org/aspectj/testing/run/IRunValidator.java new file mode 100644 index 000000000..a341aa167 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/IRunValidator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/run/RunIterator.java b/testing/src/test/java/org/aspectj/testing/run/RunIterator.java new file mode 100644 index 000000000..1b5c8f307 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/RunIterator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + + +package org.aspectj.testing.run; + +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; + +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.bridge.MessageUtil; +import org.aspectj.util.LangUtil; + +/** + * 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/test/java/org/aspectj/testing/run/RunListener.java b/testing/src/test/java/org/aspectj/testing/run/RunListener.java new file mode 100644 index 000000000..0d7c9fc76 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/RunListener.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + + +package org.aspectj.testing.run; + +import java.io.PrintWriter; +import java.util.List; + +import org.aspectj.bridge.AbortException; +import org.aspectj.bridge.MessageUtil; + +/** + * 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/test/java/org/aspectj/testing/run/RunListeners.java b/testing/src/test/java/org/aspectj/testing/run/RunListeners.java new file mode 100644 index 000000000..4eb8e7a74 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/RunListeners.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/run/RunStatus.java b/testing/src/test/java/org/aspectj/testing/run/RunStatus.java new file mode 100644 index 000000000..ac22280ba --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/RunStatus.java @@ -0,0 +1,442 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.run; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.aspectj.bridge.IMessage; +import org.aspectj.bridge.IMessageHolder; +import org.aspectj.bridge.MessageHandler; +import org.aspectj.testing.util.BridgeUtil; +import org.aspectj.util.LangUtil; + +/** + * 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.run.IRunStatus#runResult() + */ + public boolean runResult() { + return validator.runPassed(this); + } + + public boolean hasAnyMessage(IMessage.Kind kind, boolean orGreater, boolean includeChildren) { + if (messageHolder.hasAnyMessage(kind, orGreater)) { + return true; + } + if (includeChildren) { + IRunStatus[] kids = getChildren(); + for (int i = 0; i < kids.length; i++) { + if (kids[i].hasAnyMessage(kind, orGreater, true)) { + return true; + } + } + } + return false; + } + + public IMessage[] getMessages(IMessage.Kind kind, boolean orGreater, boolean includeChildren) { + IMessage[] result = getMessages(kind, orGreater); + if (!includeChildren) { + return result; + } + ArrayList sink = new ArrayList(); + if (!LangUtil.isEmpty(result)) { + sink.addAll(Arrays.asList(result)); + } + + IRunStatus[] kids = getChildren(); + for (int i = 0; i < kids.length; i++) { + result = kids[i].getMessages(kind, orGreater, includeChildren); + if (!LangUtil.isEmpty(result)) { + sink.addAll(Arrays.asList(result)); + } + } + return (IMessage[]) sink.toArray(new IMessage[0]); + } + + // ------------------- 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); + } + + public void dontIgnore(IMessage.Kind kind) { + messageHolder.dontIgnore(kind); + } + + public void ignore(IMessage.Kind kind) { + messageHolder.ignore(kind); + } + + /** + * @see org.aspectj.bridge.IMessageHolder#hasAnyMessage(org.aspectj.bridge.IMessage.Kind, boolean) + */ + public boolean hasAnyMessage(IMessage.Kind kind, boolean orGreater) { + return messageHolder.hasAnyMessage(kind, orGreater); + } + + /** + * @see org.aspectj.bridge.IMessageHolder#getMessages(org.aspectj.bridge.IMessage.Kind, boolean) + */ + public IMessage[] getMessages(IMessage.Kind kind, boolean orGreater) { + return messageHolder.getMessages(kind, orGreater); + } + + /** + * @see org.aspectj.bridge.IMessageHolder#numMessages(org.aspectj.bridge.IMessage.Kind, boolean) + */ + 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/test/java/org/aspectj/testing/run/RunValidator.java b/testing/src/test/java/org/aspectj/testing/run/RunValidator.java new file mode 100644 index 000000000..70131f71a --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/RunValidator.java @@ -0,0 +1,210 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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: + *
  • state: fail unless completed and not aborted
  • + *
  • messages: fail if any of type ABORT, FAIL - permit ERROR, WARNING, DEBUG... + * (which permits expected compiler errors and warnings)
  • + *
  • thrown: if type required, fail unless type thrown; + * if type permitted, fail if wrong type thrown
  • + *
  • result: fail unless no ObjectChecker or it validates + * and the result is not IRunStatus.FAIL.
  • + *
  • otherwise delegates to any subclass doPassed()
  • + * Client setup the expected and permitted exception classes + * and the result object checker, and may also subclass to + * query the IRunStatus more carefully. + *

    + * 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, + * except that Integer Objects must have value 0 */ + public static final IRunValidator NORMAL + = new RunValidator(ObjectChecker.ANY_ZERO); + /** expect normal completion with any non-null result object */ + public static final IRunValidator ORIGINAL_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/test/java/org/aspectj/testing/run/Runner.java b/testing/src/test/java/org/aspectj/testing/run/Runner.java new file mode 100644 index 000000000..58860be6f --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/Runner.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + + +package org.aspectj.testing.run; + +import java.util.Enumeration; +import java.util.Hashtable; + +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; + +/** + * Run IRun, setting status and invoking listeners + * for simple and nested runs. + *

    + * 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. + *

    + * 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)} + *

    + * 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. + *

    + * 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 + * iterator.iterationCompleted() 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/test/java/org/aspectj/testing/run/WrappedRunIterator.java b/testing/src/test/java/org/aspectj/testing/run/WrappedRunIterator.java new file mode 100644 index 000000000..75be92cb7 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/run/WrappedRunIterator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/taskdefs/AjcTaskCompileCommand.java b/testing/src/test/java/org/aspectj/testing/taskdefs/AjcTaskCompileCommand.java new file mode 100644 index 000000000..1f5325e82 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/taskdefs/AjcTaskCompileCommand.java @@ -0,0 +1,294 @@ +/* ******************************************************************* + * Copyright (c) 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Wes Isberg initial implementation + * ******************************************************************/ + +package org.aspectj.testing.taskdefs; + +//import java.awt.Frame; +import java.io.*; +//import java.lang.reflect.*; +import java.util.*; +//import java.util.List; + +import org.apache.tools.ant.*; +import org.apache.tools.ant.Project; +import org.aspectj.bridge.*; +import org.aspectj.tools.ant.taskdefs.AjcTask; +import org.aspectj.util.*; +import org.aspectj.util.FileUtil; + +/** + * Drive tests using the Ant taskdef. + * The non-incremental case is quite easy to implement, + * but incremental compiles require running the compiler + * in another thread using an incremental tag file. + * This is imprecise because it assumes + * incremental compiles are complete + * when messages stop coming from the compiler. + * Also, the client should call quit() when done compiling + * to halt the process. + * XXX build up ICommand with quit (and done-with-last-invocation?) + * to avoid the kludge workarounds + */ +public class AjcTaskCompileCommand implements ICommand { + /** + * 20 seconds of quiet in message holder + * before we assume incremental compile is done + */ + public static int COMPILE_SECONDS_WITHOUT_MESSAGES = 20; + + /** 5 minutes maximum time to wait for a compile to complete */ + public static int COMPILE_MAX_SECONDS = 300; + + /** + * Wait until this holder has not changed the number of messages + * in secs seconds, as a weak form of determining when the + * compile has completed. + * XXX implement "compile-complete" message instead. + * @param holder the IMessageHolder to wait for + * @param seconds the number of seconds that the messages should be the same + * @param timeoutSeconds the int number of seconds after which to time out + * @return true if the messages quiesced before the timeout + * false if parameters are 0 or negative or if + * seconds => timeoutSeconds + */ + static boolean waitUntilMessagesQuiet( + IMessageHolder holder, + int seconds, + int timeoutSeconds) { + LangUtil.throwIaxIfNull(holder, "holder"); + if ((0 >= timeoutSeconds) || (0 >= seconds) + || (timeoutSeconds <= seconds)) { + return false; + } + long curTime = System.currentTimeMillis(); + final long timeout = curTime + (timeoutSeconds*1000); +// final Thread thread = Thread.currentThread(); + final long targetQuietTime = 1000 * seconds; + int numMessages = holder.numMessages(null, true); + long numMessagesTime = curTime; + // check for new messages every .5 to 3 seconds + final long checkInterval; + { + long interval = seconds / 10; + if (interval > 3000) { + interval = 3000; + } else if (interval < 200) { + interval = 500; + } + checkInterval = interval; + } + while ((curTime < timeout) + && (curTime < (numMessagesTime + targetQuietTime))) { + // delay until next check + long nextCheck = curTime + checkInterval; + while (nextCheck > curTime) { + try { + Thread.sleep(nextCheck - curTime); + } catch (InterruptedException e) { + // ignore + } + curTime = System.currentTimeMillis(); + } + int newNumMessages = holder.numMessages(null, true); + if (newNumMessages != numMessages) { + numMessages = newNumMessages; + numMessagesTime = curTime; + } + } + return (curTime >= (numMessagesTime + targetQuietTime)); + } + + // single-threaded driver + MessageHandler messages = new MessageHandler(); + AjcTask ajcTask; + File incrementalFile; + Thread incrementalCompileThread; + + /** + * Stop waiting for any further incremental compiles. + * Safe to call in non-incremental modes. + */ + public void quit() { // XXX requires downcast from ICommand, need validator,IMessageHandler interface + AjcTask task = ajcTask; + if (null != task) { + task.quit(); // signal task to quit, thread to die + ajcTask = null; // XXX need for cleanup? + } + updateIncrementalFile(false, true); + Thread thread = incrementalCompileThread; + if (null != thread) { + if (thread.isAlive()) { + try { + thread.join(3000); + } catch (InterruptedException e) { + // ignore + } + if (thread.isAlive()) { + String s = "WARNING: abandoning undying thread "; + System.err.println(s + thread.getName()); + } + } + incrementalCompileThread = null; + } + } + + // --------- ICommand interface + public boolean runCommand(String[] args, IMessageHandler handler) { + return (makeAjcTask(args, handler) + && doCommand(handler, false)); + } + + /** + * Fails if called before runCommand or if + * not in incremental mode or if the command + * included an incremental file entry. + * @return + */ + public boolean repeatCommand(IMessageHandler handler) { + return doCommand(handler, true); + } + + protected boolean doCommand(IMessageHandler handler, boolean repeat) { + messages.clearMessages(); + if (null == ajcTask) { + MessageUtil.fail(messages, "ajcTask null - repeat without do"); + } + try { + // normal, non-incremental case + if (!repeat && (null == incrementalFile)) { + ajcTask.execute(); + // rest is for incremental mode + } else if (null == incrementalFile) { + MessageUtil.fail(messages, "incremental mode not specified"); + } else if (!updateIncrementalFile(false, false)) { + MessageUtil.fail(messages, "can't update incremental file"); + } else if (!repeat) { // first incremental compile + incrementalCompileThread = new Thread( + new Runnable() { + public void run() { + ajcTask.execute(); + } + }, "AjcTaskCompileCommand-incremental"); + incrementalCompileThread.start(); + waitUntilMessagesQuiet(messages, 10, 180); + } else { + Thread thread = incrementalCompileThread; + if (null == thread) { + MessageUtil.fail(messages, "incremental process stopped"); + } else if (!thread.isAlive()) { + MessageUtil.fail(messages, "incremental process dead"); + } else { + waitUntilMessagesQuiet(messages, 10, 180); + } + } + } catch (BuildException e) { + Throwable t = e.getCause(); + while (t instanceof BuildException) { + t = ((BuildException) t).getCause(); + } + if (null == t) { + t = e; + } + MessageUtil.abort(messages, "BuildException " + e.getMessage(), t); + } finally { + MessageUtil.handleAll(handler, messages, false); + } + return (0 == messages.numMessages(IMessage.ERROR, true)); + } + + + protected boolean makeAjcTask(String[] args, IMessageHandler handler) { + ajcTask = null; + incrementalFile = null; + AjcTask result = null; + try { + result = new AjcTask(); + Project project = new Project(); + project.setName("AjcTaskCompileCommand"); + result.setProject(project); + result.setMessageHolder(messages); + // XXX argh - have to strip out "-incremental" + // because tools.ajc.Main privileges it over tagfile + ArrayList newArgs = new ArrayList(); + boolean incremental = false; + for (int i = 0; i < args.length; i++) { + if ("-incremental".equals(args[i])) { + incremental = true; + } else if ("-XincrementalFile".equals(args[i])) { + // CommandController.TAG_FILE_OPTION = "-XincrementalFile"; + incremental = true; + i++; + } else { + newArgs.add(args[i]); + } + } + if (incremental) { + args = (String[]) newArgs.toArray(new String[0]); + } + + result.readArguments(args); + + if (incremental || result.isInIncrementalMode()) { + // these should be impossible... + if (result.isInIncrementalFileMode()) { + String m = "incremental file set in command"; + MessageUtil.fail(handler, m); + return false; + } else if (null != incrementalFile) { + String m = "incremental file set already"; + MessageUtil.fail(handler, m); + return false; + } + String prefix = "AjcTaskCompileCommand_makeAjcTask"; + File tmpDir = FileUtil.getTempDir(prefix); + incrementalFile = new File(tmpDir, "tagfile.tmp"); + boolean create = true; + boolean delete = false; + updateIncrementalFile(create, delete); + result.setTagFile(incrementalFile); + } + // do not throw BuildException on error + result.setFailonerror(false); + ajcTask = result; + } catch (BuildException e) { + MessageUtil.abort(handler,"setting up AjcTask", e); + } + return (null != ajcTask); + } + + protected boolean updateIncrementalFile(boolean create, boolean delete) { + File file = incrementalFile; + if (delete) { + try { + return (null == file) + || !file.exists() + || file.delete(); + } finally { + incrementalFile = null; + } + } + if (null == file) { + return false; + } + if (file.exists()) { + return file.setLastModified(System.currentTimeMillis()); + } else { + try { + return create && file.createNewFile(); + } catch (IOException e) { // XXX warn in verbose mode? + return false; + } + } + } + +} + diff --git a/testing/src/test/java/org/aspectj/testing/taskdefs/TaskdefTests.java b/testing/src/test/java/org/aspectj/testing/taskdefs/TaskdefTests.java deleted file mode 100644 index 5a86f8558..000000000 --- a/testing/src/test/java/org/aspectj/testing/taskdefs/TaskdefTests.java +++ /dev/null @@ -1,29 +0,0 @@ -/* ******************************************************************* - * Copyright (c) 2003 Contributors. - * All rights reserved. - * This program and the accompanying materials are made available - * under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Wes Isberg initial implementation - * ******************************************************************/ - -package org.aspectj.testing.taskdefs; - -import junit.framework.*; - -public class TaskdefTests extends TestCase { - - public static Test suite() { - TestSuite suite = new TestSuite(TaskdefTests.class.getName()); - //$JUnit-BEGIN$ - suite.addTestSuite(AjcTaskCompileCommandTest.class); - //$JUnit-END$ - return suite; - } - - public TaskdefTests(String name) { super(name); } - -} diff --git a/testing/src/test/java/org/aspectj/testing/util/AccumulatingFileFilter.java b/testing/src/test/java/org/aspectj/testing/util/AccumulatingFileFilter.java new file mode 100644 index 000000000..e01f71874 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/AccumulatingFileFilter.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/BridgeUtil.java b/testing/src/test/java/org/aspectj/testing/util/BridgeUtil.java new file mode 100644 index 000000000..f06e93850 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/BridgeUtil.java @@ -0,0 +1,517 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util; + +import java.io.File; +import java.util.Comparator; + +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; + +/** + * + */ +public class BridgeUtil { + +// private static final String INDENT = " "; + + /** result value when writeMessage is passed null */ + private static final String 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(UtilLineReader 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.getSourceLocation(); // XXX implement + } + + + public static class Comparators { + /** + * Compare based solely on null-inequality: + * -1 if one is not null and two is null, + * 1 if one is null and two is not null, + * 0 otherwise. + */ + static int compareNull(Object one, Object two) { + return (null == one + ? (null == two ? 0 : 1) + : (null == two ? -1 : 0)); + } + + /** + * Soft comparison of String returns 0 if either is empty + * or a substring of the other, and the case-insensitive + * ordering otherwise. + * @param lhs_s + * @param rhs_s + * @return + */ + static int compareStringsSoftly(String lhs_s, String rhs_s) { + if (LangUtil.isEmpty(lhs_s) + || LangUtil.isEmpty(rhs_s)) { + return 0; + } + if ((-1 != lhs_s.indexOf(rhs_s)) + || (-1 != rhs_s.indexOf(lhs_s))) { + return 0; + } + return String.CASE_INSENSITIVE_ORDER.compare(lhs_s, rhs_s); + } + + /** + * This returns 0 if one file path is a suffix of the other + * or a case-insensitive string comparison otherwise. + * WARNING: it returns 0 if either file is + * ISourceLocation.NO_FILE to permit tests to + * not specify file paths. + * + * Use only for sorts, not to maintain maps. + */ + public static final Comparator WEAK_File = new Comparator() { + public int compare(Object o1, Object o2) { + if ((o1 == o2) + || (o1 == ISourceLocation.NO_FILE) + || (o2 == ISourceLocation.NO_FILE) ) { + return 0; + } + int result = compareNull(o1, o2); + if (0 != result) { + return result; + } + File one = (File) o1; + File two = (File) o2; + String s1 = one.getPath(); + String s2 = two.getPath(); + // check if normalize needed + if (s1.endsWith(s2) || s2.endsWith(s1)) { + return 0; + } + s1 = FileUtil.weakNormalize(s1); + s2 = FileUtil.weakNormalize(s2); + if (s1.endsWith(s2) || s2.endsWith(s1)) { + return 0; + } + return String.CASE_INSENSITIVE_ORDER.compare(s1, s2); + + } + }; + /** + * Ordering only uses line number. + * Use only for sorts, not to maintain maps. + */ + public static final Comparator WEAK_ISourceLocation = new Comparator() { + public int compare(Object o1, Object o2) { + if (o1 == o2) { + return 0; + } + int result = compareNull(o1, o2); + if (0 != result) { + return result; + } + ISourceLocation one = (ISourceLocation) o1; + ISourceLocation two = (ISourceLocation) o2; + int i1 = one.getLine(); + int i2 = two.getLine(); + return i1 - i2; + } + }; + + /** + * Like WEAK_ISourceLocation, except it also + * uses WEAK_FILE on the sourceFile. + * Use only for sorts, not to maintain maps. + */ + public static final Comparator MEDIUM_ISourceLocation = new Comparator() { + public int compare(Object o1, Object o2) { + int result = WEAK_ISourceLocation.compare(o1, o2); + if (0 != result) { + return result; + } + ISourceLocation one = (ISourceLocation) o1; + ISourceLocation two = (ISourceLocation) o2; + result = compareNull(one, two); + if (0 != result) { // one but not other is null + return result; + } + if (null == one) { // both null + return 0; + } + // neither null + return WEAK_File.compare(one.getSourceFile(), two.getSourceFile()); + } + }; + + /** + * Ordering uses kind and weak source location, + * and ignores 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) { + if (o1 == o2) { + return 0; + } + int result = compareNull(o1, o2); + if (0 != result) { + return result; + } + IMessage one = (IMessage) o1; + IMessage two = (IMessage) o2; + IMessage.Kind kind1 = one.getKind(); + IMessage.Kind kind2= two.getKind(); + result = IMessage.Kind.COMPARATOR.compare(kind1, kind2); + if (0 != result) { + return result; + } + ISourceLocation sl1 = one.getSourceLocation(); + ISourceLocation sl2 = two.getSourceLocation(); + return WEAK_ISourceLocation.compare(sl1, sl2); + } + }; + + /** + * Ordering uses line and weak filename and message + * (message matches if either is a substring of the other, + * or if either is empty, i.e., none specified). + * so use only for sorts, not to maintain maps + */ + public static final Comparator MEDIUM_IMessage = new Comparator() { + public int compare(Object o1, Object o2) { + int result = WEAK_IMessage.compare(o1, o2); + if (0 != result) { + return result; + } + IMessage rhs_m= (IMessage) o1; + IMessage lhs_m = (IMessage) o2; + ISourceLocation rhs_sl = rhs_m.getSourceLocation(); + ISourceLocation lhs_sl = lhs_m.getSourceLocation(); + result = MEDIUM_ISourceLocation.compare(lhs_sl, rhs_sl); + if (0 != result) { + return result; + } + String lhs_s =lhs_m.getMessage(); + String rhs_s = rhs_m.getMessage(); + return compareStringsSoftly(lhs_s, rhs_s); + } + }; +} + 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 + 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/test/java/org/aspectj/testing/util/CollectorFileFilter.java b/testing/src/test/java/org/aspectj/testing/util/CollectorFileFilter.java new file mode 100644 index 000000000..7c63c3b6c --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/CollectorFileFilter.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/Diffs.java b/testing/src/test/java/org/aspectj/testing/util/Diffs.java new file mode 100644 index 000000000..a6edf0d87 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/Diffs.java @@ -0,0 +1,547 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util; + +import java.io.File; +import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import org.aspectj.bridge.IMessage; +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.bridge.ISourceLocation; +import org.aspectj.bridge.MessageUtil; +import org.aspectj.util.FileUtil; +import org.aspectj.util.LangUtil; + +/** + * Result struct for expected/actual diffs for Collection + */ +public class Diffs { + + /** + * Compare IMessage.Kind based on kind priority. + */ + public static final Comparator KIND_PRIORITY = new Comparator() { + /** + * Compare IMessage.Kind based on kind priority. + * @throws NullPointerException if anything is null + */ + public int compare(Object lhs, Object rhs) { + return ((IMessage.Kind) lhs).compareTo((IMessage.Kind) rhs); + } + }; + /** + * Sort ISourceLocation based on line, file path. + */ + public static final Comparator SORT_SOURCELOC = new Comparator() { + /** + * Compare ISourceLocation based on line, file path. + * @throws NullPointerException if anything is null + */ + public int compare(Object lhs, Object rhs) { + ISourceLocation l = (ISourceLocation) lhs; + ISourceLocation r = (ISourceLocation) rhs; + int result = getLine(l) - getLine(r); + if (0 != result) { + return result; + } + String lp = getSourceFile(l).getPath(); + String rp = getSourceFile(r).getPath(); + return lp.compareTo(rp); + } + }; + + /** + * Compare IMessages based on kind and source location line (only). + */ + public static final Comparator MESSAGE_LINEKIND = new Comparator() { + /** + * Compare IMessages based on kind and source location line (only). + * @throws NullPointerException if anything is null + */ + public int compare(Object lhs, Object rhs) { + IMessage lm = (IMessage) lhs; + IMessage rm = (IMessage) rhs; + ISourceLocation ls = (lm == null ? null : lm.getSourceLocation()); + ISourceLocation rs = (rm == null ? null : rm.getSourceLocation()); + int left = (ls == null ? -1 : ls.getLine()); + int right = (rs == null ? -1 : rs.getLine()); + int result = left - right; + if (0 == result) { + result = lm.getKind().compareTo(rm.getKind()); + } + return result; + } + }; + public static final Filter ACCEPT_ALL = new Filter() { + public boolean accept(Object o) { + return true; + } + }; + // // XXX List -> Collection b/c comparator orders + // public static final Diffs NONE + // = new Diffs("NONE", Collections.EMPTY_LIST, Collections.EMPTY_LIST); + + public static Diffs makeDiffs( + String label, + List expected, + List actual, + Comparator comparator) { + return makeDiffs( + label, + expected, + actual, + comparator, + ACCEPT_ALL, + ACCEPT_ALL); + } + + public static Diffs makeDiffs( + String label, + IMessage[] expected, + IMessage[] actual) { + return makeDiffs(label, expected, actual, null, null); + } + + private static int getLine(ISourceLocation loc) { + int result = -1; + if (null != loc) { + result = loc.getLine(); + } + return result; + } + private static int getLine(IMessage message) { + int result = -1; + if ((null != message)) { + result = getLine(message.getSourceLocation()); + } + return result; + } + + private static File getSourceFile(ISourceLocation loc) { + File result = ISourceLocation.NO_FILE; + if (null != loc) { + result = loc.getSourceFile(); + } + return result; + } + + public static Diffs makeDiffs( + String label, + IMessage[] expected, + IMessage[] actual, + IMessage.Kind[] ignoreExpectedKinds, + IMessage.Kind[] ignoreActualKinds) { + ArrayList exp = getExcept(expected, ignoreExpectedKinds); + ArrayList act = getExcept(actual, ignoreActualKinds); + + ArrayList missing = new ArrayList(); + List unexpected = new ArrayList(); + + if (LangUtil.isEmpty(expected)) { + unexpected.addAll(act); + } else if (LangUtil.isEmpty(actual)) { + missing.addAll(exp); + } else { + ListIterator expectedIterator = exp.listIterator(); + int lastLine = Integer.MIN_VALUE + 1; + ArrayList expectedFound = new ArrayList(); + ArrayList expectedForLine = new ArrayList(); + for (ListIterator iter = act.listIterator(); iter.hasNext();) { + IMessage actualMessage = (IMessage) iter.next(); + int actualLine = getLine(actualMessage); + if (actualLine != lastLine) { + // new line - get all messages expected for it + if (lastLine > actualLine) { + throw new Error("sort error"); + } + lastLine = actualLine; + expectedForLine.clear(); + while (expectedIterator.hasNext()) { + IMessage curExpected = + (IMessage) expectedIterator.next(); + int curExpectedLine = getLine(curExpected); + if (actualLine == curExpectedLine) { + expectedForLine.add(curExpected); + } else { + expectedIterator.previous(); + break; + } + } + } + // now check actual against all expected on that line + boolean found = false; + IMessage expectedMessage = null; + for (Iterator iterator = expectedForLine.iterator(); + !found && iterator.hasNext(); + ) { + expectedMessage = (IMessage) iterator.next(); + found = expectingMessage(expectedMessage, actualMessage); + } + if (found) { + iter.remove(); + if (expectedFound.contains(expectedMessage)) { + // XXX warn: expected message matched two actual + } else { + expectedFound.add(expectedMessage); + } + } else { + // unexpected: any actual result not found + unexpected.add(actualMessage); + } + } + // missing: all expected results not found + exp.removeAll(expectedFound); + missing.addAll(exp); + } + return new Diffs(label, missing, unexpected); + } + + public static Diffs makeDiffs( + String label, + List expected, + List actual, + Comparator comparator, + Filter missingFilter, + Filter unexpectedFilter) { + label = label.trim(); + if (null == label) { + label = ": "; + } else if (!label.endsWith(":")) { + label += ": "; + } + final String thisLabel = " " + label; + ArrayList miss = new ArrayList(); + ArrayList unexpect = new ArrayList(); + + org.aspectj.testing.util.LangUtil.makeSoftDiffs( + expected, + actual, + miss, + unexpect, + comparator); + if (null != missingFilter) { + for (ListIterator iter = miss.listIterator(); iter.hasNext();) { + if (!missingFilter.accept(iter.next())) { + iter.remove(); + } + } + } + if (null != unexpectedFilter) { + for (ListIterator iter = unexpect.listIterator(); + iter.hasNext(); + ) { + if (!unexpectedFilter.accept(iter.next())) { + iter.remove(); + } + } + } + return new Diffs(thisLabel, miss, unexpect); + } + + // /** + // * Shift over elements in sink if they are of one of the specified kinds. + // * @param sink the IMessage[] to shift elements from + // * @param kinds + // * @return length of sink after shifting + // * (same as input length if nothing shifted) + // */ + // public static int removeKinds(IMessage[] sink, IMessage.Kind[] kinds) { + // if (LangUtil.isEmpty(kinds)) { + // return sink.length; + // } else if (LangUtil.isEmpty(sink)) { + // return 0; + // } + // int from = -1; + // int to = -1; + // for (int j = 0; j < sink.length; j++) { + // from++; + // if (null == sink[j]) { + // continue; + // } + // boolean remove = false; + // for (int i = 0; !remove && (i < kinds.length); i++) { + // IMessage.Kind kind = kinds[i]; + // if (null == kind) { + // continue; + // } + // if (0 == kind.compareTo(sink[j].getKind())) { + // remove = true; + // } + // } + // if (!remove) { + // to++; + // if (to != from) { + // sink[to] = sink[from]; + // } + // } + // } + // return to+1; + // } + + /** + * @param expected the File from the expected source location + * @param actual the File from the actual source location + * @return true if exp is ISourceLocation.NO_FILE + * or exp path is a suffix of the actual path + * (after using FileUtil.weakNormalize(..) on both) + */ + static boolean expectingFile(File expected, File actual) { + if (null == expected) { + return (null == actual); + } else if (null == actual) { + return false; + } + if (expected != ISourceLocation.NO_FILE) { + String expPath = FileUtil.weakNormalize(expected.getPath()); + String actPath = FileUtil.weakNormalize(actual.getPath()); + if (!actPath.endsWith(expPath)) { + return false; + } + } + return true; + } + + /** + * Soft comparison for expected message will not check a corresponding + * element in the actual message unless defined in the expected message. + *

    +	 *   message
    +	 *     kind                must match (constant/priority)
    +	 *     message             only requires substring
    +	 *     thrown              ignored
    +	 *     column              ignored
    +	 *     endline             ignored
    +	 *     details             only requires substring
    +	 *     sourceLocation
    +	 *       line              must match, unless expected < 0
    +	 *       file              ignored if ISourceLocation.NOFILE
    +	 *                         matches if expected is a suffix of actual
    +	 *                         after changing any \ to /
    +	 *     extraSourceLocation[]
    +	 *                         if any are defined in expected, then there
    +	 *                         must be exactly the actual elements as are
    +	 *                         defined in expected (so it is an error to 
    +	 *                         not define all if you define any)
    +	 * 
    +	 * @param expected
    +	 * @param actual
    +	 * @return true if we are expecting the line, kind, file, message,
    +	 *    details, and any extra source locations.
    +	 *    (ignores column/endline, thrown) XXX
    +	 */
    +	static boolean expectingMessage(IMessage expected, IMessage actual) {
    +		if (null == expected) {
    +			return (null == actual);
    +		} else if (null == actual) {
    +			return false;
    +		}
    +		if (0 != expected.getKind().compareTo(actual.getKind())) {
    +			return false;
    +		}
    +		if (!expectingSourceLocation(expected.getSourceLocation(),
    +			actual.getSourceLocation())) {
    +			return false;
    +		}
    +		if (!expectingText(expected.getMessage(), actual.getMessage())) {
    +			return false;
    +		}
    +		if (!expectingText(expected.getDetails(), actual.getDetails())) {
    +			return false;
    +		}
    +		ISourceLocation[] esl =
    +			(ISourceLocation[]) expected.getExtraSourceLocations().toArray(
    +				new ISourceLocation[0]);
    +		ISourceLocation[] asl =
    +			(ISourceLocation[]) actual.getExtraSourceLocations().toArray(
    +				new ISourceLocation[0]);
    +
    +		Arrays.sort(esl, SORT_SOURCELOC);
    +		Arrays.sort(asl, SORT_SOURCELOC);
    +		if (!expectingSourceLocations(esl, asl)) {
    +			return false;
    +		}
    +		return true;
    +	}
    +
    +	/**
    +	 * This returns true if no ISourceLocation are specified
    +	 * (i.e., it ignored any extra source locations if no expectations stated).
    +	 * XXX need const like NO_FILE.
    +	 * @param expected the sorted ISourceLocation[] expected
    +	 * @param expected the actual sorted ISourceLocation[] 
    +	 * @return true if any expected element is expected by the corresponding actual element.
    +	 */
    +	static boolean expectingSourceLocations(
    +		ISourceLocation[] expected,
    +		ISourceLocation[] actual) {
    +		if (LangUtil.isEmpty(expected)) {
    +			return true;
    +		} else if (LangUtil.isEmpty(actual)) {
    +			return false;
    +		} else if (actual.length != expected.length) {
    +			return false;
    +		}
    +		for (int i = 0; i < actual.length; i++) {
    +			if (!expectingSourceLocation(expected[i], actual[i])) {
    +				return false;
    +			}
    +		}
    +
    +		return true;
    +	}
    +
    +	/**
    +	 * @param expected
    +	 * @param actual
    +	 * @return true if any expected line/file matches the actual line/file,
    +	 *         accepting a substring as a file match
    +	 */
    +	static boolean expectingSourceLocation(
    +		ISourceLocation expected,
    +		ISourceLocation actual) {
    +        int eline = getLine(expected);
    +        int aline = getLine(actual);
    +		if ((-1 < eline) && (eline != aline)) {
    +			return false;
    +		}
    +		if (!expectingFile(getSourceFile(expected), getSourceFile(actual))) {
    +			return false;
    +		}
    +		return true;
    +	}
    +
    +	/**
    +	 * @param expected the String in the expected message
    +	 * @param actual the String in the actual message
    +	 * @return true if both are null or actual contains expected
    +	 */
    +	static boolean expectingText(String expected, String actual) {
    +		if (null == expected) {
    +			return true; // no expectations
    +		} else if (null == actual) {
    +			return false; // expected something
    +		} else {
    +			return (-1 != actual.indexOf(expected));
    +		}
    +	}
    +
    +	private static ArrayList getExcept(
    +		IMessage[] source,
    +		IMessage.Kind[] skip) {
    +		ArrayList sink = new ArrayList();
    +		if (LangUtil.isEmpty(source)) {
    +			return sink;
    +		}
    +
    +		if (LangUtil.isEmpty(skip)) {
    +			sink.addAll(Arrays.asList(source));
    +			Collections.sort(sink, MESSAGE_LINEKIND);
    +			return sink;
    +		}
    +		for (int i = 0; i < source.length; i++) {
    +			IMessage message = source[i];
    +			IMessage.Kind mkind = message.getKind();
    +			boolean skipping = false;
    +			for (int j = 0; !skipping && (j < skip.length); j++) {
    +				if (0 == mkind.compareTo(skip[j])) {
    +					skipping = true;
    +				}
    +			}
    +			if (!skipping) {
    +				sink.add(message);
    +			}
    +		}
    +		Collections.sort(sink, MESSAGE_LINEKIND);
    +		return sink;
    +	}
    +
    +	private static List harden(List list) {
    +		return (
    +			LangUtil.isEmpty(list)
    +				? Collections.EMPTY_LIST
    +				: Collections.unmodifiableList(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;
    +
    +	/**
    +	 * Struct-constructor stores these values,
    +	 * wrapping the lists as unmodifiable.
    +	 * @param label the String label for these diffs
    +	 * @param missing the List of missing elements
    +	 * @param unexpected the List of unexpected elements
    +	 */
    +	public Diffs(String label, List missing, List unexpected) {
    +		this.label = label;
    +		this.missing = harden(missing);
    +		this.unexpected = harden(unexpected);
    +		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 
    +	 * MessageUtil.renderMessage(IMessage).
    +	 * @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()
    +			+ ")";
    +	}
    +	public static interface Filter {
    +		/** @return true to keep input in list of messages */
    +		boolean accept(Object input);
    +	}
    +}
    diff --git a/testing/src/test/java/org/aspectj/testing/util/FileUtil.java b/testing/src/test/java/org/aspectj/testing/util/FileUtil.java
    new file mode 100644
    index 000000000..db77cbd10
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/FileUtil.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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[] 
    +     *   { "-copy", "-srcFile" | "-srcUrl", {src}, "-destFile", {destFile} }
    +     */
    +    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:
    +     * 
    +     * 
    Diffs d = dirDiffs(dir, givenTime, null, null, null);
    + * + * Given files + *
    classes/Foo.class
    +     * classes/bar/Bash.class
    +     * classes/Old.class
    +     * classes/one/Unexpected.class
    +     * classes/start.gif
    + * where only Old.class predated startTime, this is a call that + * reports "one/Unexpected.class" as unexpected and "Foo" + * as missing: + *
    String requireSuffix = ".class";
    +     * String[] expectedPaths = new String[] { "Foo", "bar/Bas" };
    +     * File file = new File("classes");
    +     * Diffs d = dirDiffs(dir, startTime, requireSuffix,expectedPaths, true);
    + * + * @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 Diffs.makeDiffs(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 enu = zipfile.entries(); + while (enu.hasMoreElements()) { + ZipEntry entry = (ZipEntry) enu.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: + *
    descendFileTree(new File("."), new FileFilter() {
    +	 *     public boolean accept(File f){
    +	 *        System.out.println(f.getAbsolutePath());
    +     *        return true;
    +	 *     }});
    + * @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: + *
    descendFileTree(new File("/"), new FileFilter() {
    +     *     public boolean run(File f){
    +     *        System.out.println(f.getAbsolutePath());
    +     *        return true;
    +     *     }}, false);
    + * To list files/dir from root using user recursion: + *
    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);
    + * @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 enu = zip.entries(); + while (enu.hasMoreElements()) { + results.add(renderZipEntry(zipfile, (ZipEntry) enu.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/test/java/org/aspectj/testing/util/IntRange.java b/testing/src/test/java/org/aspectj/testing/util/IntRange.java new file mode 100644 index 000000000..1f44f69bd --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/IntRange.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/IntValidator.java b/testing/src/test/java/org/aspectj/testing/util/IntValidator.java new file mode 100644 index 000000000..e3697d1aa --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/IntValidator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/IteratorWrapper.java b/testing/src/test/java/org/aspectj/testing/util/IteratorWrapper.java new file mode 100644 index 000000000..5b7c13af9 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/IteratorWrapper.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/LangUtil.java b/testing/src/test/java/org/aspectj/testing/util/LangUtil.java new file mode 100644 index 000000000..2baf81c48 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/LangUtil.java @@ -0,0 +1,1271 @@ +/* ******************************************************************* + * Copyright (c) 1999-2000 Xerox Corporation. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + + +package org.aspectj.testing.util; + +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; + +import org.aspectj.bridge.AbortException; +import org.aspectj.bridge.IMessage; + + +/** + * 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); + } + } + + /** + * Get indexes of any invalid entries in array. + * @param ra the Object[] entries to check + * (if null, this returns new int[] { -1 }) + * @param superType the Class, if any, to verify that + * any entries are assignable. + * @return null if all entries are non-null, assignable to superType + * or comma-delimited error String, with components + * "[#] {null || not {superType}", + * e.g., "[3] null, [5] not String" + */ + public static String invalidComponents(Object[] ra, Class superType) { + if (null == ra) { + return "null input array"; + } else if (0 == ra.length) { + return null; + } + StringBuffer result = new StringBuffer(); + final String cname = LangUtil.unqualifiedClassName(superType); +// int index = 0; + for (int i = 0; i < ra.length; i++) { + if (null == ra[i]) { + result.append(", [" + i + "] null"); + } else if ((null != superType) + && !superType.isAssignableFrom(ra[i].getClass())) { + result.append(", [" + i + "] not " + cname); + } + } + if (0 == result.length()) { + return null; + } else { + return result.toString().substring(2); + } + } + + /** @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())); + } + + + /** + * Throw IllegalArgumentException if any component in input array + * is null or (if superType is not null) not assignable to superType. + * The exception message takes the form + * {name} invalid entries: {invalidEntriesResult} + * @throws IllegalArgumentException if any components bad + * @see #invalidComponents(Object[], Class) + */ + public static final void throwIaxIfComponentsBad( + final Object[] input, + final String name, + final Class superType) { + String errs = invalidComponents(input, superType); + if (null != errs) { + String err = name + " invalid entries: " + errs; + throw new IllegalArgumentException(err); + } + } + /** + * 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 + *
    SPLIT_START {string} { SPLIT_DELIM {string}} SPLIT_END
    +     * (e.g., "[one, two, three]").
    +     */
    +    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();
    +    }
    +
    +    /**
    +     * debugStr(o, false);
    +     * @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] = "";
    +                }
    +			}
    +            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] = "";
    +            } 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 "";
    +                    }
    +                }
    +            });
    +        } catch (PrivilegedActionException e) {
    +            return "";
    +        }               
    +    }
    +    
    +    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;
    +    }
    +
    +    /**  debugStr(source, names, items, null, null, null, null) */ 
    +    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:
    +     * {className}{prefix}{{name}{infix}{value}{delimiter}}..{suffix}
    +     * Structure[head=root, tail=leaf]
    + * Passing null for the formatting entries provokes the default values, + * so to print nothing, you should pass "". Default values:
    +     *    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);
    +        
    +        ArrayList actual = new ArrayList();
    +        actual.addAll(actualListIn);
    +        Collections.sort(actual, comparator);        
    +        Iterator actualIter = actual.iterator();        
    +        Object act = null;
    +        
    +        if (missingListOut != null) {
    +        	missingListOut.addAll(expectedListIn);
    +        }
    +        if (extraListOut != null) {
    +        	extraListOut.addAll(actualListIn);
    +        }
    +        
    +        // AMC: less efficient, but simplified implementation. Needed since messages can
    +        // now match on text content too, and the old algorithm did not cope with two expected
    +        // messages on the same line, but with different text content.
    +        while (actualIter.hasNext()) {
    +        	act = actualIter.next();
    +        	for (Iterator expectedIter = expected.iterator(); expectedIter.hasNext();) {
    +        		Object exp = expectedIter.next();
    +        		// if actual matches expected remove actual from extraListOut, and 
    +        		// remove expected from missingListOut
    +        		int diff = comparator.compare(exp,act);
    +        		if (diff == 0) {
    +        			extraListOut.remove(act);
    +        			missingListOut.remove(exp);
    +        		} else if (diff > 0) {
    +        			// since list is sorted, there can be no more matches...
    +        			break;
    +        		}
    +        	}
    +        }
    +        
    +//        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 = "";
    +        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/test/java/org/aspectj/testing/util/LineReader.java b/testing/src/test/java/org/aspectj/testing/util/LineReader.java
    new file mode 100644
    index 000000000..a6af8266a
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/LineReader.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/LinkCheck.java b/testing/src/test/java/org/aspectj/testing/util/LinkCheck.java
    new file mode 100644
    index 000000000..3f1ef92a1
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/LinkCheck.java
    @@ -0,0 +1,546 @@
    +/* *******************************************************************
    + * Copyright (c) 2003 Contributors. 
    + * All rights reserved. 
    + * This program and the accompanying materials are made available 
    + * under the terms of the Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-v10.html 
    + *  
    + * Contributors: 
    + *     Wes Isberg     initial implementation 
    + * ******************************************************************/
    +
    +package org.aspectj.testing.util;
    +
    +import java.io.*;
    +import java.net.*;
    +import java.util.*;
    +
    +import javax.swing.text.MutableAttributeSet;
    +import javax.swing.text.html.*;
    +import javax.swing.text.html.HTML.Tag;
    +
    +import org.aspectj.bridge.*;
    +import org.aspectj.bridge.IMessageHandler;
    +import org.aspectj.util.LangUtil;
    +//import org.aspectj.util.FileUtil;
    +
    +/**
    + * Quick and dirty link checker.
    + * This checks that links into file: and http: targets work,
    + * and that links out of file: targets work.
    + */
    +public class LinkCheck {
    + /*
    +  * Known issues:
    +  * - main interface only, though API's easily upgraded
    +  * - https MalformedUrlExceptions on redirect
    +  * - Swing won't quit without System.exit
    +  * - single-threaded
    +  */   
    +    static final URL COMMAND_LINE;
    +    static {
    +        URL commandLine = null;
    +        try {
    +            commandLine = new URL("file://commandLine");
    +        } catch (Throwable t) {
    +        }
    +        COMMAND_LINE = commandLine;
    +    }
    +
    +    /** @param args file {-logFile {file} | -printInfo } */
    +    public static void main(String[] args) {
    +        final String syntax = "java " 
    +        + LinkCheck.class.getName() 
    +        + " file {-log  | -printInfo}..";
    +        if ((null == args) || (0 >= args.length)) {
    +            System.err.println(syntax);
    +            System.exit(1);
    +        }
    +        final String startingURL = "file:///" + args[0].replace('\\', '/');
    +        String logFile = null;
    +        boolean printInfo = false;
    +        for (int i = 1; i < args.length; i++) {
    +            if ("-log".equals(args[i]) && ((i+1) < args.length)) {
    +                logFile = args[++i];
    +            } else if ("-printInfo".equals(args[i])) {
    +                printInfo = true;
    +            } else {
    +                System.err.println(syntax);
    +                System.exit(1);                
    +            }
    +        }
    +        final boolean useSystemOut = (null == logFile);
    +        final MessageHandler mh;
    +        final OutputStream out;
    +        if (useSystemOut) {
    +            mh = new MessageHandler();
    +            out = null;
    +        } else {
    +        
    +            try {
    +                out = new FileOutputStream(logFile);
    +            } catch (FileNotFoundException e) {
    +                e.printStackTrace();
    +                return;
    +            }
    +            final PrintStream ps = new PrintStream(out, true);
    +            final boolean printAll = printInfo;
    +            mh = new MessageHandler() {
    +                public boolean handleMessage(IMessage message) {
    +                    if (printAll || !message.isInfo()) {
    +                        ps.println(message.toString());
    +                    }
    +                    return super.handleMessage(message);
    +                }
    +
    +            };
    +        }
    +        Link.Check exists 
    +            = Link.getProtocolChecker(new String[] {"file", "http"});
    +        Link.Check contents 
    +            = Link.getProtocolChecker(new String[] {"file"});
    +        LinkCheck me = new LinkCheck(mh, exists, contents);
    +        me.addLinkToCheck(COMMAND_LINE, startingURL); // pwd as base?
    +        try {
    +            String label = "checking URLs from " + startingURL;
    +            if (useSystemOut) {
    +                System.out.println(label);
    +            }
    +            MessageUtil.info("START " + label);
    +            long start = System.currentTimeMillis();
    +            me.run();
    +            long duration = (System.currentTimeMillis() - start)/1000;
    +            long numChecked = me.checkedUrls.size();
    +            if (numChecked > 0) {
    +                float cps = (float) duration  / (float) numChecked;
    +                StringBuffer sb = new StringBuffer();
    +                sb.append("DONE. Checked " + numChecked);
    +                sb.append(" URL's in " + duration);
    +                sb.append(" seconds (" + cps);
    +                sb.append(" seconds per URL).");
    +                MessageUtil.info("END " + label + ": " + sb);
    +                if (useSystemOut) {
    +                    System.out.println(sb.toString());
    +                }
    +            }
    +            MessageUtil.info(MessageUtil.renderCounts(mh));
    +            try {
    +                if (null != out) {
    +                    out.flush();
    +                } 
    +            } catch (IOException e) {
    +                // ignore
    +            }
    +            if (useSystemOut && (null != logFile)) {
    +                System.out.println("Find log in " + logFile);
    +            }
    +        } finally {
    +            if (null != out) {
    +                try {
    +                    out.close();
    +                } catch (IOException e1) {
    +                }
    +            }
    +            System.exit(mh.numMessages(IMessage.ERROR, true)); // XXX dumb swing
    +        }
    +    }
    +
    +//    private static boolean isCheckedFileType(URL url) {
    +//        if (null == url) {
    +//            return false;
    +//        } 
    +//        String file = url.getFile();
    +//        return !FileUtil.hasZipSuffix(file)
    +//            && !file.endsWith(".pdf");
    +//    }
    +    
    +    private final Messages messages;
    +    private final HTMLEditorKit.Parser parser; // XXX untested - stateful
    +    private final ArrayList linksToCheck; // Link
    +    private final ArrayList checkedUrls;  // String (URL.toString)
    +    private final ArrayList validRefs;  // String (URL.toString)
    +    private final ArrayList refsToCheck;  // String (URL.toString)
    +    
    +    private final Link.Check checkExists;
    +    private final Link.Check checkContents;
    +
    +    public LinkCheck(IMessageHandler handler, 
    +        Link.Check checkExists,
    +        Link.Check checkContents) {
    +        LangUtil.throwIaxIfNull(handler, "handler");
    +        LangUtil.throwIaxIfNull(checkExists, "checkExists");
    +        LangUtil.throwIaxIfNull(checkContents, "checkContents");
    +        this.messages = new Messages(handler);
    +        linksToCheck = new ArrayList();
    +        checkedUrls = new ArrayList();
    +        refsToCheck = new ArrayList();
    +        validRefs = new ArrayList();
    +        parser = new HTMLEditorKit() {
    +            public HTMLEditorKit.Parser getParser() {
    +                return super.getParser();
    +            }
    +        }
    +        .getParser();
    +        this.checkExists = checkExists;
    +        this.checkContents = checkContents;
    +    }
    +
    +    public synchronized void addLinkToCheck(URL doc, String link) {
    +        URL linkURL = makeURL(doc, link);
    +        if (null == linkURL) {
    +//            messages.addingNullLinkFrom(doc);
    +            return;
    +        }
    +        String linkString = linkURL.toString();
    +        if ((null != link) && !checkedUrls.contains(linkString) ) {
    +            if (!checkExists.check(linkURL)) {
    +                checkedUrls.add(linkString);
    +                messages.acceptingUncheckedLink(doc, linkURL);
    +            } else {
    +                Link toAdd = new Link(doc, linkURL);
    +                if (!linksToCheck.contains(toAdd)) { // equals overridden
    +                    linksToCheck.add(toAdd);
    +                }
    +            }
    +        }
    +    }
    +
    +    public synchronized void run() {
    +        ArrayList list = new ArrayList();
    +        while (0 < linksToCheck.size()) {
    +            messages.checkingLinks(linksToCheck.size());
    +            list.clear();
    +            list.addAll(linksToCheck);
    +            for (Iterator iter = list.iterator(); iter.hasNext();) {
    +                final Link link = (Link) iter.next();
    +                String urlString = link.url.toString();
    +                if (!checkedUrls.contains(urlString)) {
    +                    checkedUrls.add(urlString);
    +                    messages.checkingLink(link);
    +                    checkLink(link);
    +                }
    +            }
    +            linksToCheck.removeAll(list);
    +        }
    +        // now check that all named references are accounted for
    +        for (Iterator iter = refsToCheck.iterator(); iter.hasNext();) {
    +            String ref = (String) iter.next();
    +            if (!validRefs.contains(ref)) {
    +                messages.namedReferenceNotFound(ref);
    +            }
    +        }
    +    }
    +
    +    /** @return null if link known or if unable to create */
    +    private URL makeURL(URL doc, String link) {
    +        if (checkedUrls.contains(link)) {
    +            return null;
    +        }
    +        URL result = null;
    +        try {
    +            result = new URL(link);
    +        } catch (MalformedURLException e) {
    +            if (null == doc) {
    +                messages.malformedUrl(null, link, e);
    +            } else {
    +                try {
    +                    URL res = new URL(doc, link);
    +                    String resultString = res.toString();
    +                    if (checkedUrls.contains(resultString)) {
    +                        return null;
    +                    }
    +                    result = res;
    +                } catch (MalformedURLException me) {
    +                    messages.malformedUrl(doc, link, me);
    +                }
    +            }
    +        }
    +        return result;
    +    }
    +
    +    /** @param link a Link with a url we can handle */
    +    private void checkLink(final Link link) {
    +        if (handleAsRef(link)) {
    +            return;
    +        }
    +        URL url = link.url;
    +        InputStream input = null;
    +        try {                        
    +            URLConnection connection = url.openConnection();
    +            if (null == connection) {
    +                messages.cantOpenConnection(url);
    +                return;
    +            }
    +            // get bad urls to fail on read before skipping by type
    +            input = connection.getInputStream();
    +            String type = connection.getContentType();
    +            if (null == type) {
    +                messages.noContentType(link);
    +            } else if (!type.toLowerCase().startsWith("text/")) {
    +                messages.notTextContentType(link);
    +            } else {
    +                boolean addingLinks = checkContents.check(url);
    +                parser.parse(
    +                    new InputStreamReader(input),
    +                    new LinkListener(url, addingLinks), true);
    +            }
    +        } catch (IOException e) {
    +            messages.exceptionReading(link, e);
    +        } finally {
    +            if (null != input) {
    +                try {
    +                    input.close();
    +                } catch (IOException e1) {
    +                    // ignore
    +                }
    +            }
    +        }
    +    }
    +
    +    /** @return true if link is to an internal ...#name */
    +    private boolean handleAsRef(Link link) {
    +        String ref = link.url.getRef();
    +        if (!LangUtil.isEmpty(ref)) {
    +            String refString = link.url.toString(); // XXX canonicalize?
    +            if (!refsToCheck.contains(refString)) {
    +                refsToCheck.add(refString);
    +                // add pseudo-link to force reading of ref'd doc XXX hmm
    +                int refLoc = refString.indexOf("#");
    +                if (-1 == refLoc) {
    +                    messages.uncommentedReference(link);
    +                } else {
    +                    refString = refString.substring(0, refLoc);
    +                    addLinkToCheck(link.doc, refString);
    +                }
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    /** LinkListener callback */
    +    private boolean addKnownNamedAnchor(URL doc, String name) {
    +        String namedRef = "#" + name;
    +        try {
    +            String ref = new URL(doc + namedRef).toString();
    +            if (!validRefs.contains(ref)) {
    +                validRefs.add(ref);
    +            }
    +            return true;
    +        } catch (MalformedURLException e) {
    +            messages.malformedUrl(doc, namedRef, e);
    +            return false;
    +        }    
    +    }
    +       
    +    private class Messages {
    +        private final IMessageHandler handler;
    +        private Messages(IMessageHandler handler) {
    +            LangUtil.throwIaxIfNull(handler, "handler");
    +            this.handler = handler;
    +        }
    +        
    +        private void info(String label, Object more) {
    +            MessageUtil.info(handler, label + " " + more);
    +        }
    +
    +        private void fail(String label, Object more, Throwable thrown) {
    +            MessageUtil.fail(handler, label + " " + more, thrown);
    +        }
    +
    +        private void uncommentedReference(Link link) {
    +            info("uncommentedReference", link); // XXX bug?
    +        }
    +
    +//        private void addingNullLinkFrom(URL doc) {
    +//            info("addingNullLinkFrom", doc);
    +//        }
    +//
    +//        private void noContentCheck(Link link) {
    +//            info("noContentCheck", link);
    +//        }
    +
    +        private void notTextContentType(Link link) {
    +            info("notTextContentType", link);
    +        }
    +
    +        private void noContentType(Link link) {
    +            info("noContentType", link);
    +        }
    +
    +        private void checkingLinks(int i) {
    +            info("checkingLinks", new Integer(i));
    +        }
    +
    +        private void checkingLink(Link link) {
    +            info("checkingLink", link);
    +        }
    +
    +        private void acceptingUncheckedLink(URL doc, URL link) {
    +            info("acceptingUncheckedLink", "doc=" + doc + " link=" + link);
    +        }
    +
    +//        private void cantHandleRefsYet(Link link) {
    +//            info("cantHandleRefsYet", link.url);
    +//        }
    +
    +        private void namedReferenceNotFound(String ref) {
    +            // XXX find all references to this unfound named reference
    +            fail("namedReferenceNotFound", ref, null);
    +        }
    +
    +        private void malformedUrl(URL doc, String link, MalformedURLException e) {
    +            fail("malformedUrl", "doc=" + doc + " link=" + link, e);
    +        }
    +        
    +        private void cantOpenConnection(URL url) {
    +            fail("cantOpenConnection", url, null);
    +        }
    +        
    +        private void exceptionReading(Link link, IOException e) {
    +            // only info if redirect from http to https
    +            String m = e.getMessage();
    +            if ((m != null) 
    +                && (-1 != m.indexOf("protocol"))
    +                && (-1 != m.indexOf("https"))
    +                && "http".equals(link.url.getProtocol())) {
    +                info("httpsRedirect", link);
    +                return;
    +            }
    +            fail("exceptionReading", link, e);
    +        }
    +        
    +        private void nullLink(URL doc, Tag tag) {
    +            // ignore - many tags do not have links
    +        }
    +        
    +        private void emptyLink(URL doc, Tag tag) {
    +            fail("emptyLink", "doc=" + doc + " tag=" + tag, null);
    +        }
    +    }
    +
    +    /**
    +     * Register named anchors and add any hrefs to the links to check.
    +     */
    +    private class LinkListener extends HTMLEditorKit.ParserCallback {
    +        private final URL doc;
    +        private final boolean addingLinks;
    +
    +        private LinkListener(URL doc, boolean addingLinks) {
    +            this.doc = doc;
    +            this.addingLinks = addingLinks;
    +        }
    +
    +        public void handleStartTag(
    +            HTML.Tag tag,
    +            MutableAttributeSet attributes,
    +            int position) {
    +            handleSimpleTag(tag, attributes, position);
    +        }
    +
    +        public void handleSimpleTag(
    +            HTML.Tag tag,
    +            MutableAttributeSet attributes,
    +            int position) { // XXX use position to emit context?
    +            boolean isNameAnchor = registerIfNamedAnchor(tag, attributes);
    +            if (!addingLinks) {
    +                return;
    +            }
    +            Object key = HTML.Tag.FRAME == tag 
    +                ? HTML.Attribute.SRC
    +                : HTML.Attribute.HREF;
    +            String link = (String) attributes.getAttribute(key);
    +
    +            if (null == link) {
    +                if (!isNameAnchor) {
    +                    messages.nullLink(doc, tag);
    +                }
    +            } else if (0 == link.length()) {
    +                if (!isNameAnchor) {
    +                    messages.emptyLink(doc, tag);
    +                }
    +            } else {
    +                addLinkToCheck(doc, link);
    +            }
    +        }
    +        
    +        private boolean registerIfNamedAnchor(
    +            HTML.Tag tag,
    +            MutableAttributeSet attributes) {
    +            if (HTML.Tag.A.equals(tag)) {
    +                String name  
    +                    = (String) attributes.getAttribute(HTML.Attribute.NAME);
    +                if (null != name) {
    +                    addKnownNamedAnchor(doc, name);
    +                    return true;
    +                }                
    +            }
    +            return false;
    +        }
    +        
    +    }
    +
    +    private static class Link {
    +        private static final Check FALSE_CHECKER = new Check() {
    +            public boolean check(Link link) { return false; }
    +            public boolean check(URL url) { return false; }
    +        };
    +        private static Check getProtocolChecker(String[] protocols) {
    +            final String[] input 
    +                = (String[]) LangUtil.safeCopy(protocols, protocols);
    +            if (0 == input.length) {
    +                return FALSE_CHECKER;
    +            }
    +            return new Check() {
    +                final List list = Arrays.asList(input);
    +                public boolean check(URL url) {
    +                    return (null != url) && list.contains(url.getProtocol());
    +                }
    +            };
    +        }
    +        private final URL doc;
    +        private final URL url;
    +        private String toString;
    +        private Link(URL doc, URL url) {
    +            LangUtil.throwIaxIfNull(doc, "doc");
    +            LangUtil.throwIaxIfNull(url, "url");
    +            this.doc = doc;
    +            this.url = url;
    +        }
    +        public boolean equals(Object o) {
    +            if (null == o) {
    +                return false;
    +            } 
    +            if (this == o) {
    +                return true;
    +            }
    +            if (Link.class != o.getClass()) {
    +                return false; // exact class
    +            }
    +            Link other = (Link) o;
    +            return doc.equals(other) && url.equals(other);
    +            //return toString().equals(o.toString());
    +        }
    +        
    +        public int hashCode() { // XXX
    +            return doc.hashCode() + (url.hashCode() >> 4);
    +//            return toString.hashCode();
    +        }
    +        
    +        public String toString() {
    +            if (null == toString) {
    +                toString = url + " linked from " + doc;
    +            }
    +            return toString;
    +        }
    +        private static class Check {
    +            public boolean check(Link link) {
    +                return (null != link) && check(link.url);
    +            }
    +            public boolean check(URL url) {
    +                return (null != url);
    +            }
    +        }
    +    }
    +}
    diff --git a/testing/src/test/java/org/aspectj/testing/util/Node.java b/testing/src/test/java/org/aspectj/testing/util/Node.java
    new file mode 100644
    index 000000000..9439e57e4
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/Node.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/NullPrintStream.java b/testing/src/test/java/org/aspectj/testing/util/NullPrintStream.java
    new file mode 100644
    index 000000000..686c9c8e2
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/NullPrintStream.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/ObjectChecker.java b/testing/src/test/java/org/aspectj/testing/util/ObjectChecker.java
    new file mode 100644
    index 000000000..25985ae5e
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/ObjectChecker.java
    @@ -0,0 +1,51 @@
    +/* *******************************************************************
    + * 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 v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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.NOT_NULL"; }
    +    };
    +    
    +    /** @return true if input is 0 Integer or any other non-Integer reference. */
    +    public static final ObjectChecker ANY_ZERO = new ObjectChecker() {
    +        public boolean isValid(Object input) { 
    +            if (input instanceof Integer) {
    +                return (0 == ((Integer) input).intValue());
    +            } else {
    +                return true;
    +            }
    +        }
    +        public String toString() { return "ObjectChecker.ANY_ZERO"; }
    +    };
    +    
    +    /**
    +     * 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/test/java/org/aspectj/testing/util/ProxyPrintStream.java b/testing/src/test/java/org/aspectj/testing/util/ProxyPrintStream.java
    new file mode 100644
    index 000000000..5c5bbe9de
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/ProxyPrintStream.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/RunUtils.java b/testing/src/test/java/org/aspectj/testing/util/RunUtils.java
    new file mode 100644
    index 000000000..86c29a4a2
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/RunUtils.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-v10.html 
    + *  
    + * Contributors: 
    + *     Xerox/PARC     initial implementation 
    + * ******************************************************************/
    +
    +package org.aspectj.testing.util;
    +
    +import java.io.PrintStream;
    +import java.util.Iterator;
    +
    +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;
    +
    +/**
    + * 
    + */
    +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/test/java/org/aspectj/testing/util/SFileReader.java b/testing/src/test/java/org/aspectj/testing/util/SFileReader.java
    new file mode 100644
    index 000000000..eecf2d304
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/SFileReader.java
    @@ -0,0 +1,184 @@
    +/* *******************************************************************
    + * 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 v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-v10.html 
    + *  
    + * Contributors: 
    + *     Xerox/PARC     initial implementation 
    + * ******************************************************************/
    +
    +package org.aspectj.testing.util;
    +
    +import java.io.File;
    +import java.io.IOException;
    +import java.io.PrintStream;
    +import java.util.ArrayList;
    +
    +import org.aspectj.bridge.AbortException;
    +import org.aspectj.bridge.IMessage;
    +import org.aspectj.util.LangUtil;
    +
    +
    +/**
    + * 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");
    +        }
    +        UtilLineReader reader = null;
    +        try {
    +            reader = UtilLineReader.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(UtilLineReader 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(UtilLineReader reader) {
    +                return reader + ": " + reader.lastLine();
    +            }
    +            public Class getType() { return String.class; }
    +        };
    +    }
    +}
    diff --git a/testing/src/test/java/org/aspectj/testing/util/StandardObjectChecker.java b/testing/src/test/java/org/aspectj/testing/util/StandardObjectChecker.java
    new file mode 100644
    index 000000000..bc2f520ad
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/StandardObjectChecker.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/StreamSniffer.java b/testing/src/test/java/org/aspectj/testing/util/StreamSniffer.java
    new file mode 100644
    index 000000000..cb143604e
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/StreamSniffer.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/StreamsHandler.java b/testing/src/test/java/org/aspectj/testing/util/StreamsHandler.java
    new file mode 100644
    index 000000000..fc69d9900
    --- /dev/null
    +++ b/testing/src/test/java/org/aspectj/testing/util/StreamsHandler.java
    @@ -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 Eclipse Public License v1.0 
    + * which accompanies this distribution and is available at 
    + * http://www.eclipse.org/legal/epl-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.
    + * 
    + * 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);
    + * 
    + * 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/test/java/org/aspectj/testing/util/StringAccumulator.java b/testing/src/test/java/org/aspectj/testing/util/StringAccumulator.java new file mode 100644 index 000000000..234592386 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/StringAccumulator.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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:
    "[{index}]"{entry}"\n
    + * + * @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/test/java/org/aspectj/testing/util/StringVisitor.java b/testing/src/test/java/org/aspectj/testing/util/StringVisitor.java new file mode 100644 index 000000000..123376df4 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/StringVisitor.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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/test/java/org/aspectj/testing/util/StructureModelUtil.java b/testing/src/test/java/org/aspectj/testing/util/StructureModelUtil.java new file mode 100644 index 000000000..10e03dd22 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/StructureModelUtil.java @@ -0,0 +1,263 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; + +import org.aspectj.asm.AsmManager; +import org.aspectj.asm.IHierarchy; +import org.aspectj.asm.IProgramElement; + +/** + * Prototype functionality for package view clients. + */ +public class StructureModelUtil { + + public static class ModelIncorrectException extends Exception { + + private static final long serialVersionUID = 8920868549577870993L; + + public ModelIncorrectException(String s) { + super(s); + } + } + + /** + * Check the properties of the current model. The parameter string lists properties of the model that should be correct. If any + * of the properties are incorrect, a ModelIncorrectException is thrown. + * + * @param toCheck comma separated list of name=value pairs that should be found in the ModelInfo object + * @throws ModelIncorrectException thrown if any of the name=value pairs in toCheck are not found + */ + public static void checkModel(String toCheck) throws ModelIncorrectException { + Properties modelProperties = AsmManager.lastActiveStructureModel.summarizeModel().getProperties(); + + // Break toCheck into pieces and check each exists + StringTokenizer st = new StringTokenizer(toCheck, ",="); + while (st.hasMoreTokens()) { + String key = st.nextToken(); + String expValue = st.nextToken(); + boolean expectingZero = false; + try { + expectingZero = (Integer.parseInt(expValue) == 0); + } catch (NumberFormatException nfe) { + // this is ok as expectingZero will be false + } + String value = modelProperties.getProperty(key); + if (value == null) { + if (!expectingZero) + throw new ModelIncorrectException("Couldn't find '" + key + "' property for the model"); + } else if (!value.equals(expValue)) { + throw new ModelIncorrectException("Model property '" + key + "' incorrect: Expected " + expValue + " but found " + + value); + } + } + } + + /** + * This method returns a map from affected source lines in a class to a List of aspects affecting that line. Based on method of + * same name by mik kirsten. To be replaced when StructureModelUtil corrects its implementation + * + * @param the full path of the source file to get a map for + * + * @return a Map from line numbers to a List of ProgramElementNodes. + */ + public static Map getLinesToAspectMap(String sourceFilePath) { + + // Map annotationsMap = + // AsmManager.getDefault().getInlineAnnotations( + // sourceFilePath, + // true, + // true); + + Map aspectMap = new HashMap(); + // Set keys = annotationsMap.keySet(); + // for (Iterator it = keys.iterator(); it.hasNext();) { + // Object key = it.next(); + // List annotations = (List) annotationsMap.get(key); + // for (Iterator it2 = annotations.iterator(); it2.hasNext();) { + // IProgramElement node = (IProgramElement) it2.next(); + + // List relations = node.getRelations(); + // + // for (Iterator it3 = relations.iterator(); it3.hasNext();) { + // IRelationship relationNode = (IRelationship) it3.next(); + + // if (relationNode.getKind().equals("Advice")) { + // List children = relationNode.getTargets(); + // + // List aspects = new Vector(); + // + // for (Iterator it4 = children.iterator(); + // it4.hasNext(); + // ) { + // Object object = it4.next(); + // + // // if (object instanceof LinkNode) { + // // IProgramElement pNode = + // // ((LinkNode) object).getProgramElementNode(); + // // + // // if (pNode.getProgramElementKind() + // // == IProgramElement.Kind.ADVICE) { + // // + // // IProgramElement theAspect = pNode.getParent(); + // // + // // aspects.add(theAspect); + // // + // // } + // // } + // } + // if (!aspects.isEmpty()) { + // aspectMap.put(key, aspects); + // } + // } + // + // } + // } + // } + return aspectMap; + } + + /** + * This method is copied from StructureModelUtil inoder for it to use the working version of getLineToAspectMap() + * + * @return the set of aspects with advice that affects the specified package + */ + public static Set getAspectsAffectingPackage(IProgramElement packageNode) { + List files = StructureModelUtil.getFilesInPackage(packageNode); + Set aspects = new HashSet(); + for (Iterator it = files.iterator(); it.hasNext();) { + IProgramElement fileNode = (IProgramElement) it.next(); + Map adviceMap = getLinesToAspectMap(fileNode.getSourceLocation().getSourceFile().getAbsolutePath()); + Collection values = adviceMap.values(); + for (Iterator it2 = values.iterator(); it2.hasNext();) { + aspects.add(it2.next()); + } + } + return aspects; + } + + public static List getPackagesInModel(AsmManager modl) { + List packages = new ArrayList(); + IHierarchy model = modl.getHierarchy(); + if (model.getRoot().equals(IHierarchy.NO_STRUCTURE)) { + return null; + } else { + return getPackagesHelper(model.getRoot(), IProgramElement.Kind.PACKAGE, null, packages); + } + } + + private static List getPackagesHelper(IProgramElement node, IProgramElement.Kind kind, String prename, List matches) { + + if (kind == null || node.getKind().equals(kind)) { + if (prename == null) { + prename = new String(node.toString()); + } else { + prename = new String(prename + "." + node); + } + Object[] o = new Object[2]; + o[0] = node; + o[1] = prename; + + matches.add(o); + } + + for (Iterator it = node.getChildren().iterator(); it.hasNext();) { + IProgramElement nextNode = (IProgramElement) it.next(); + getPackagesHelper(nextNode, kind, prename, matches); + } + + return matches; + } + + /** + * Helper function sorts a list of resources into alphabetical order + */ + // private List sortElements(List oldElements) { + // Object[] temp = oldElements.toArray(); + // SortingComparator comparator = new SortingComparator(); + // + // Arrays.sort(temp, comparator); + // + // List newResources = Arrays.asList(temp); + // + // return newResources; + // } + // + // private static List sortArray(List oldElements) { + // Object[] temp = oldElements.toArray(); + // SortArrayComparator comparator = new SortArrayComparator(); + // + // Arrays.sort(temp, comparator); + // + // List newElements = Arrays.asList(temp); + // + // return newElements; + // } + // private class SortingComparator implements Comparator { + // public int compare(Object o1, Object o2) { + // IProgramElement p1 = (IProgramElement) o1; + // IProgramElement p2 = (IProgramElement) o2; + // + // String name1 = p1.getName(); + // String name2 = p2.getName(); + // + // return name1.compareTo(name2); + // } + // } + // + // private static class SortArrayComparator implements Comparator { + // public int compare(Object o1, Object o2) { + // Object[] array1 = (Object[]) o1; + // Object[] array2 = (Object[]) o2; + // + // IProgramElement p1 = (IProgramElement) array1[1]; + // IProgramElement p2 = (IProgramElement) array2[1]; + // + // String name1 = p1.getName(); + // String name2 = p2.getName(); + // + // return name1.compareTo(name2); + // } + // } + /** + * @return all of the AspectJ and Java source files in a package + */ + public static List getFilesInPackage(IProgramElement packageNode) { + List packageContents; + if (packageNode == null) { + return null; + } else { + packageContents = packageNode.getChildren(); + } + List files = new ArrayList(); + for (Iterator it = packageContents.iterator(); it.hasNext();) { + IProgramElement packageItem = (IProgramElement) it.next(); + if (packageItem.getKind() == IProgramElement.Kind.FILE_JAVA + || packageItem.getKind() == IProgramElement.Kind.FILE_ASPECTJ) { + files.add(packageItem); + } + } + return files; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/util/TestClassLoader.java b/testing/src/test/java/org/aspectj/testing/util/TestClassLoader.java new file mode 100644 index 000000000..dd9c10c8f --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/TestClassLoader.java @@ -0,0 +1,157 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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 { + + /** seek classes in dirs first */ + List /*File*/ dirs; + + /** save URL[] only for toString */ + private URL[] urlsForDebugString; + + public TestClassLoader(URL[] urls, File[] dirs) { + super(urls); + this.urlsForDebugString = urls; + LangUtil.throwIaxIfComponentsBad(dirs, "dirs", null); + ArrayList dcopy = new ArrayList(); + + if (!LangUtil.isEmpty(dirs)) { + 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; + } + + /** @return null if class not found or byte[] of class otherwise */ + private byte[] readClass(String className) throws ClassNotFoundException { + 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); + } + } + return null; + } + + 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; + } + + /** @return String with debug info: urls and classes used */ + public String toString() { + return "TestClassLoader(urls=" + + Arrays.asList(urlsForDebugString) + + ", dirs=" + + dirs + + ")"; + } +} + diff --git a/testing/src/test/java/org/aspectj/testing/util/TestDiffs.java b/testing/src/test/java/org/aspectj/testing/util/TestDiffs.java new file mode 100644 index 000000000..54f72e9b9 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/TestDiffs.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util; + +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; + +import org.aspectj.util.LangUtil; + +/** + * 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 -traceTestsMin 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 = Diffs.makeDiffs("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 = Diffs.makeDiffs("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 -traceTestsmin). + * @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/test/java/org/aspectj/testing/util/UtilLineReader.java b/testing/src/test/java/org/aspectj/testing/util/UtilLineReader.java new file mode 100644 index 000000000..ebb6c9d98 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/UtilLineReader.java @@ -0,0 +1,212 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + + +package org.aspectj.testing.util; + +import java.io.*; +import java.util.ArrayList; + +/** + * LineNumberReader which absorbs comments and blank lines + * and renders as file:line + */ +public class UtilLineReader 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 UtilLineReader createTester(File file) { + return create(file, TESTER_LEAD, null); + } + + /** + * convenience factory + * @return null if IOException or IllegalArgumentException thrown + */ + public static final UtilLineReader create(File file, + String[] leadComments, String[] eolComments) { + try { + FileReader reader = new FileReader(file); + return new UtilLineReader(reader, file, leadComments, eolComments); + } catch (IllegalArgumentException e) { + } catch (IOException e) { + } + return null; + } + + final private File file; + final private String[] eolComments; + final private String[] leadComments; + transient String lastLine; + + /** + * @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 UtilLineReader(FileReader reader, File file, + String[] leadComments, String[] eolComments) { + super(reader); + this.file = file; + this.eolComments = normalize(eolComments); + this.leadComments = normalize(leadComments); + } + public UtilLineReader(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(); + } + + /** + * lastLine is set only by readClippedLine, not readLine. + * @return the last line read, after clipping + */ + public String lastLine() { + return lastLine; + } + + /** + * 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 skipEmpties if true, run to next non-empty line; if false, return next line + * @return null if no more lines or got an empty line when they are not ok, + * or next non-null, non-empty line in reader, + * ignoring comments + */ + public String nextLine(boolean skipEmpties) throws IOException { + String result; + do { + result = readClippedLine(); + if ((null != result) && skipEmpties && (0 == result.length())) { + continue; + } + return result; + } while (true); + } + + /** @return null if no more lines or a clipped line otherwise */ + protected String readClippedLine() throws IOException { + String result = readLine(); + if (result != null) { + result = result.trim(); + int len = result.length(); + for (int i = 0; ((0 < len) && (i < leadComments.length)); i++) { + if (result.startsWith(leadComments[i])) { + result = ""; + len = 0; + } + } + for (int i = 0; ((0 < len) && (i < eolComments.length)); i++) { + int loc = result.indexOf(eolComments[i]); + if (-1 != loc) { + result = result.substring(0, loc); + len = result.length(); + } + } + } + lastLine = result; + 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/test/java/org/aspectj/testing/util/UtilTests.java b/testing/src/test/java/org/aspectj/testing/util/UtilTests.java deleted file mode 100644 index f5583725d..000000000 --- a/testing/src/test/java/org/aspectj/testing/util/UtilTests.java +++ /dev/null @@ -1,39 +0,0 @@ -/* ******************************************************************* - * 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 v1.0 - * which accompanies this distribution and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Xerox/PARC initial implementation - * ******************************************************************/ - - -package org.aspectj.testing.util; - -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; - -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); - suite.addTestSuite(StructureModelUtilTest.class); - //$JUnit-END$ - return suite; - } - - public UtilTests(String name) { super(name); } - -} diff --git a/testing/src/test/java/org/aspectj/testing/util/ValidFileFilter.java b/testing/src/test/java/org/aspectj/testing/util/ValidFileFilter.java new file mode 100644 index 000000000..8d069af0f --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/ValidFileFilter.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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 FileFilter.accept(File) 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/test/java/org/aspectj/testing/util/WebInstall.java b/testing/src/test/java/org/aspectj/testing/util/WebInstall.java new file mode 100644 index 000000000..3433d55b6 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/WebInstall.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-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: + *

    java -classpath aj-testing.jar org.aspectj.testing.util.WebInstall + * http://aspectj.org/download/distribution/aspectj10-tools.jar -text + * install.properties + * + *

    You can omit the -text install.properties if there is + * a file called "install.properties" in the current directory. + * + *

    The properties file must define the properties + * output.dir and context.javaPath + * 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: + *

    output.dir=c:\\output\\dir
    + * context.javaPath=c:\\apps\\jdk1.3.1
    + * + * 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. + * + *

    You may also use this as a driver for the known installers + * by specifying the following options (-baseurl must be first):

    + *         -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) 
    + * such that URL= + * {baseurl}{version}.jar + * 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 install.properties + * is in current directory */ + private static final String[] ARGS = new String[] + { "-text", "install.properties" }; + + /** @param args the String[] { "" {, "-text", "" } */ + 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
    +     *         -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) 
    + * such that URL= + * {baseurl}{version}.jar + * 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/test/java/org/aspectj/testing/util/options/Option.java b/testing/src/test/java/org/aspectj/testing/util/options/Option.java new file mode 100644 index 000000000..e845a89bf --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/options/Option.java @@ -0,0 +1,707 @@ +/* ******************************************************************* + * Copyright (c) 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Wes Isberg initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util.options; + +import java.util.*; + +import org.aspectj.util.LangUtil; + +/** + * Immutable schema for an input (command-line) option. + * The schema contains the expected name/label, + * the family (for comparison purposes), + * and permitted prefixes. + * This has operations to accept input values and compare options. + * Options cannot be created directly; for that, use an + * Option.Factory, since it enforces uniqueness + * within the families and options created by the factory. + *

    + * Option is used with related nested classes to implement relations: + *

      + *
    • Option.Factory produces Option
    • + *
    • An Option has a set of Option.Prefixes, + * which are variants of Option.Prefix + * valid for the option (e.g., on/set, force-off, and force-on)
    • + *
    • Option evaluates input, produces Option.Value
    • + *
    • Related instances of Option share an Option.Family, + * which enforce option exclusion, etc.
    • + *
    + * The classes are nested as "friends" in order to hide private + * members (esp. constructors) that can be used within relations. + * Most of these classes are immutable. + */ +public class Option implements Comparable { + public static final Prefix ON = new Prefix("-", "on", true, false); + public static final Prefix NONE = new Prefix("", "none", true, false); + public static final Prefix FORCE_ON = + new Prefix("!", "force-on", true, true); + + public static final Prefix FORCE_OFF = + new Prefix("^", "force-off", false, true); + public static final Prefixes LITERAL_PREFIXES = + new Prefixes(new Prefix[] { NONE }); + public static final Prefixes STANDARD_PREFIXES = + new Prefixes(new Prefix[] { ON }); + public static final Prefixes FORCE_PREFIXES = + new Prefixes(new Prefix[] { ON, FORCE_ON, FORCE_OFF }); + + /** family is the key for comparing two options */ + private final Family family; + + /** label expected for the option */ + private final String name; + + /** unique identifier for option */ + private final String optionIdentifier; + + /** prefixes permitted for the option in input */ + private final Prefixes permittedPrefixes; + + /** if true, then match on input that has extra suffix beyond prefix and name */ + private final boolean acceptSuffixedInput; + + /** + * If true, no collision if there are multiple values + * that share the same family but not the same literal value + */ + private final boolean permitMultipleValues; + + /** int number of arguments expected after the option itself */ + private final int numArguments; + + /** + * If numArguments > 0, each element has a list of arguments + * permitted at that index from the initial matching value. + * Elements are not null. + */ + private final String[][] permittedArguments; + + private final int nameLength; + + /* + * Create a standard named boolean option, + * permitting force-on and force-off. + * @param name the String name of the option, e.g., "1.3" for "-1.3" + * @param family + * @param permittedPrefixes + * @param acceptSuffixedInput + * @param permittedArguments + */ + public Option( + String name, + Family family, + Prefixes permittedPrefixes, + boolean acceptSuffixedInput, + String[][] permittedArguments) { + LangUtil.throwIaxIfNull(name, "name"); + LangUtil.throwIaxIfNull(family, "family"); + LangUtil.throwIaxIfNull(permittedPrefixes, "permittedPrefixes"); + this.name = name; + this.nameLength = name.length(); + this.family = family; + this.permittedPrefixes = permittedPrefixes; + this.acceptSuffixedInput = acceptSuffixedInput; + this.permitMultipleValues = false; + if (LangUtil.isEmpty(permittedArguments)) { + permittedArguments = new String[][] { }; + // nominal, unused + } else { + String[][] temp = new String[permittedArguments.length][]; + for (int i = 0; i < temp.length; i++) { + String[] toCopy = permittedArguments[i]; + LangUtil.throwIaxIfNull(toCopy, "no permitted args"); + final int len = toCopy.length; + String[] variants = new String[len]; + System.arraycopy(toCopy, 0, variants, 0, len); + temp[i] = variants; + } + permittedArguments = temp; + } + this.permittedArguments = permittedArguments; + numArguments = permittedArguments.length; + optionIdentifier = family.familyName + "." + name; + } + + public int compareTo(Object other) { + Option option = (Option) other; + int diff = family.compareTo(option.family); + if (0 == diff) { + diff = name.compareTo(option.name); + } + return diff; + } + + public Family getFamily() { + return family; + } + + public boolean permitMultipleValues() { + return permitMultipleValues; + } + + /** + * @return int number of elements in this option, + * e.g., 0 for -g or 1 for -source 1.4 + */ + public int numArguments() { + return numArguments; + } + + /** + * If this value String represents a valid input for this option, + * then create and return the associated Value. + * + * @param value the Value created, or null if invalid + * @return Value if this value is permitted by this option + */ + public Value acceptValue(String value) { + Prefix prefix = hasPrefix(value); + if (null != prefix) { + if (value.startsWith(name, prefix.length())) { + value = value.substring(prefix.length()); + if (value.length() == nameLength) { + return new Value(value, prefix, this); + } else if (acceptSuffixedInput) { + return new Value(value, prefix, this); + } else { + return rejectingSuffixedInput(value); + } + } + } + return null; + } + + /** @return true if we have same option family */ + public boolean sameOptionFamily(Option other) { + return ((null != other) && other.family.equals(family)); + } + + /** @return true if we have same option family and name */ + public boolean sameOptionIdentifier(Option other) { + return (sameOptionFamily(other) && name.equals(other.name)); + } + + public String toString() { + return name; + } + + /** + * Called when ignoreSuffix is off but we got value with suffix. + */ + protected Value rejectingSuffixedInput(String value) { + return null; + } + + /** + * Verify that the input is permitted at this position. + * @param input the String input to check for validity + * @param position the int proposed position (0-based) + * for the input (position 0 is for first argument) + * @return null if this input is valid at this position, + * or a String error message otherwise. + */ + String validArgument(String input, int position) { + if (null == input) { + return "null input"; + } + // assert numArguments == permittedInput.length + if ((position < 0) || (position >= numArguments)) { + return "no input permitted at " + position; + } + String[] permitted = permittedArguments[position]; + for (int i = 0; i < permitted.length; i++) { + if (input.equals(permitted[i])) { + return null; + } + } + return input + " not permitted, expecting one of " + + Arrays.asList(permitted); + } + + String getName() { + return name; + } + Object getKey() { + return family; + } + + private String optionIdentifier() { + return optionIdentifier; + } + + private Prefix hasPrefix(String value) { + for (Iterator iter = permittedPrefixes.iterator(); + iter.hasNext(); + ) { + Prefix prefix = (Prefix) iter.next(); + if (-1 != prefix.prefixLength(value)) { + return prefix; + } + } + return null; + } + + /** + * An option family identifies a set of related options that + * might share no literal specification. + * E.g., the compiler family of options might include + * -ajc and -eclipse, and the debugInfo family of options + * might include -g and -g:vars. + * Option families may permit or avoid option collisions. + *

    + * For subclasses to permit some collisions and not others, + * they should set permitMultipleFamilyValues to false + * and implement doCollision(Option, Option). + *

    + * This relies on Factory to ensure that familyName is + * a unique identifier for the factory. + */ + public static class Family implements Comparable { + + /** unique String identifier for this family */ + private final String familyName; + + /** if true, then report no collisions */ + private final boolean permitMultipleFamilyValues; + + protected Family( + String familyName, + boolean permitMultipleFamilyValues) { + this.familyName = familyName; + this.permitMultipleFamilyValues = permitMultipleFamilyValues; + } + + public int compareTo(Object arg0) { + Family family = (Family) arg0; + return familyName.compareTo(family.familyName); + } + + public boolean sameFamily(Family family) { + return ( + (null != family) && familyName.equals(family.familyName)); + } + + boolean permitMultipleFamilyValues() { + return permitMultipleFamilyValues; + } + + /** + * Options collide if they share the same family + * but are not the same, + * and multiple values are not permitted by the family. + * @param lhs the Option to compare with rhs + * @param rhs the Option to compare with lhs + * @return true if the two options collide, false otherwise + * @throws IllegalArgumentException if the input differ + * and share the same family, but this isn't it. + */ + public final boolean collision(Option lhs, Option rhs) { + if ((lhs == rhs) || (null == lhs) || (null == rhs)) { + return false; + } + Family lhsFamily = lhs.getFamily(); + Family rhsFamily = rhs.getFamily(); + if (!(lhsFamily.sameFamily(rhsFamily))) { + return false; + } + if (lhs.sameOptionIdentifier(rhs)) { + return false; + } + if (this != lhsFamily) { + String s = + "expected family " + this +" got family " + lhsFamily; + throw new IllegalArgumentException(s); + } + return doCollision(lhs, rhs); + } + + /** + * Subclasses implement this to resolve collisions on + * a case-by-case basis. Input are guaranteed to be + * non-null, different, and to share this family. + * This implementation returns + * !permitMultipleFamilyValues. + * + * @param lhs the Option to compare + * @param rhs the other Option to compare + * @return true if there is a collision. + */ + protected boolean doCollision(Option lhs, Option rhs) { + return !permitMultipleFamilyValues; + } + } + + /** + * A factory enforces a namespace on options. + * All options produced from a given factory are unique, + * as are all families. + * Once an option or family is created, it cannot be changed. + * To have a family permit multiple values + * (i.e., ignore collisions), set up the family before any + * associated options are created using + * setupFamily(String, boolean). + */ + public static class Factory { + private final String factoryName; + + /** enforce uniqueness of family */ + private final Map familyNameToFamily = new TreeMap(); + + /** enforce uniqueness of options */ + private final ArrayList names = new ArrayList(); + + public Factory(String factoryName) { + this.factoryName = factoryName; + } + + /** + * Ensure that the family with this name has the + * specified permission. If the family does not exist, + * it is created. If it does, the permission is checked. + * If this returns false, there is no way to change the + * family permission. + * @param name the String identifier for the family + * @param permitMultipleValues the boolean permission whether to + * allow multiple values in this family + * @return true if family exists with this name and permission + */ + public boolean setupFamily( + String name, + boolean permitMultipleValues) { + LangUtil.throwIaxIfNull(name, "name"); + Family family; + synchronized (familyNameToFamily) { + family = (Family) familyNameToFamily.get(name); + if (null == family) { + family = new Family(name, permitMultipleValues); + familyNameToFamily.put(name, family); + } else if ( + permitMultipleValues + != family.permitMultipleFamilyValues) { + return false; + } + } + return true; + } + + /** + * Register a family with this factory. + * @return null if the family was successfully registered, + * or a String error otherwise + */ + public String registerFamily(Family family) { + if (null == family) { + return "null family"; + } + synchronized (familyNameToFamily) { + Family knownFamily = + (Family) familyNameToFamily.get(family.familyName); + if (null == knownFamily) { + familyNameToFamily.put(family.familyName, family); + } else if (!knownFamily.equals(family)) { + return "different family registered, have " + + knownFamily + + " registering " + + family; + } + } + + return null; + } + + public Option create(String name) { + return create(name, name, FORCE_PREFIXES, false); + } + + public Option create( + String name, + String family, + Prefixes permittedPrefixes, + boolean acceptSuffixedInput) { + return create( + name, + family, + permittedPrefixes, + acceptSuffixedInput, + (String[][]) null); + } + + public Option create( + String name, + String family, + Prefixes permittedPrefixes, + boolean acceptSuffixedInput, + String[][] permittedArguments) { + LangUtil.throwIaxIfNull(name, "name"); + LangUtil.throwIaxIfNull(family, "family"); + LangUtil.throwIaxIfNull( + permittedPrefixes, + "permittedPrefixes"); + Family resolvedFamily; + synchronized (familyNameToFamily) { + resolvedFamily = (Family) familyNameToFamily.get(family); + if (null == resolvedFamily) { + resolvedFamily = new Family(family, false); + familyNameToFamily.put(family, resolvedFamily); + } + } + Option result = + new Option( + name, + resolvedFamily, + permittedPrefixes, + acceptSuffixedInput, + permittedArguments); + synchronized (names) { + String optionIdentifier = result.optionIdentifier(); + if (names.contains(optionIdentifier)) { + String s = "not unique: " + result; + throw new IllegalArgumentException(s); + } else { + names.add(optionIdentifier); + } + } + return result; + } + + public String toString() { + return Factory.class.getName() + ": " + factoryName; + } + +// private void checkUnique(Option result) { +// String name = result.family + "." + result.name; +// } + } + + /** + * The actual input value for an option. + * When an option takes arguments, all the arguments + * are absorbed/flattened into its value. + */ + public static class Value { + private static final String FLATTEN_DELIM = "_"; + + private static final int NOTARGUMENT = -1; + + private static String flatten(String prefix, String suffix) { + return prefix + FLATTEN_DELIM + suffix; + } + + private static String[] unflatten(Value value) { + if (value.argIndex == Value.NOTARGUMENT) { + return new String[] { value.value }; + } + StringTokenizer st = + new StringTokenizer(value.value, FLATTEN_DELIM); + String[] result = new String[st.countTokens()]; + // assert result.length == 1+inputIndex + for (int i = 0; i < result.length; i++) { + result[i] = st.nextToken(); + } + return result; + + } + + public final String value; + public final Prefix prefix; + public final Option option; + private final int argIndex; + + private Value( + String value, + Prefix prefix, + Option option) { + this(value, prefix, option, NOTARGUMENT); + } + private Value( + String value, + Prefix prefix, + Option option, + int argIndex) { + this.value = value; + this.prefix = prefix; + this.option = option; + this.argIndex = argIndex; + // asserts deferred - local clients only + // assert null != value + // assert null != prefix + // assert null != option + // assert 0 <= inputIndex + // assert inputIndex <= option.numArguments() + // assert {number of DELIM} == argIndex + } + + public String[] unflatten() { + return unflatten(this); + } + + /** + * Create new value same as this, but with new prefix. + * If the prefix is the same, return this. + * @param prefix the Prefix to convert to + * @return Value with new prefix - never null + */ + public Value convert(Prefix prefix) { + LangUtil.throwIaxIfNull(prefix, "prefix"); + if (this.prefix.equals(prefix)) { + return this; + } + return new Value( + this.value, + prefix, + this.option, + this.argIndex); + } + + /** + * + * @param other + * @return true if other == this for purposes of collisions + */ + public boolean sameValueIdentifier(Value other) { + return ( + (null != other) + && sameValueIdentifier(option, other.value)); + } + + public boolean sameValueIdentifier(Option option, String value) { + return ( + (null != option) + && this.option.sameOptionIdentifier(option) + && this.value.equals(value)); + } + + public boolean conflictsWith(Value other) { + return ( + (null != other) + && option.equals(other.option) + && (prefix.force == other.prefix.force) + && ((prefix.set != other.prefix.set) + || !value.equals(other.value))); + } + + public String toString() { + return option + "=" + prefix + value; + } + + final Value nextInput(String input) throws InvalidInputException { + final int index = argIndex + 1; + String err = option.validArgument(input, index); + if (null != err) { + throw new InvalidInputException(err, input, option); + } + return new Value(flatten(value, input), prefix, option, index); + } + } + + /** + * A bunch of prefixes. + */ + public static class Prefixes { + final List list; + private Prefixes(Prefix[] prefixes) { + if (LangUtil.isEmpty(prefixes)) { + list = Collections.EMPTY_LIST; + } else { + list = + Collections.unmodifiableList( + Arrays.asList( + LangUtil.safeCopy(prefixes, new Prefix[0]))); + } + } + public Iterator iterator() { + return list.iterator(); + } + } + + /** + * A permitted prefix for an option, mainly so that options + * "-verbose", "^verbose", and "!verbose" can be treated + * as variants (set, force-off, and force-on) of the + * same "verbose" option. + */ + public static class Prefix { + private final String prefix; + private final int prefixLength; + private final String name; + private final boolean set; + private final boolean force; + private Prefix( + String prefix, + String name, + boolean set, + boolean force) { + this.prefix = prefix; + this.name = name; + this.set = set; + this.force = force; + this.prefixLength = prefix.length(); + } + + /** + * Render a value for input if this is set. + * @param value the String to render as an input value + * @return null if value is null or option is not set, + * "-" + value otherwise + */ + public String render(String value) { + return ((!set || (null == value)) ? null : "-" + value); + } + + boolean forceOff() { + return force && !set; + } + boolean forceOn() { + return force && set; + } + public boolean isSet() { + return set; + } + private int length() { + return prefixLength; + } + private int prefixLength(String input) { + if ((null != input) && input.startsWith(prefix)) { + return length(); + } + return -1; + } + public String toString() { + return prefix; + } + } + + /** + * Thrown when an Option specifies required arguments, + * but the arguments are not available. + */ + public static class InvalidInputException extends Exception { + public final String err; + public final String input; + public final Option option; + InvalidInputException(String err, String input, Option option) { + super(err); + this.err = err; + this.input = input; + this.option = option; + } + public String getFullMessage() { + return "illegal input \"" + + input + + "\" for option " + + option + + ": " + + err; + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/util/options/Options.java b/testing/src/test/java/org/aspectj/testing/util/options/Options.java new file mode 100644 index 000000000..04a968d88 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/options/Options.java @@ -0,0 +1,152 @@ +/* ******************************************************************* + * Copyright (c) 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Wes Isberg initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util.options; + +import java.util.*; + +import org.aspectj.util.LangUtil; + +/** + * A bunch of options that handle search boilerplate. + * This enforces an initialization phase by permitting + * options to be added only until frozen, and + * permitting matching only after frozen. + */ +public class Options { + + /** if true, then perform extra checks to debug problems */ +// private static final boolean verifying = false; + private static final boolean FROZEN = true; + + /** + * List input unmatched by options, if any. + * @param input the String[] used to generate the values + * @param values the Option.Value[] found from the input + * @return null if no values are null, String list of missed otherwise + */ + public static String missedMatchError( + String[] input, + Values values) { + int[] missed = values.indexMissedMatches(); + LangUtil.throwIaxIfNull(input, "input"); + LangUtil.throwIaxIfNull(values, "values"); + LangUtil.throwIaxIfFalse( + input.length == values.length(), + "input is not matched by values"); + if (0 == missed.length) { + return null; + } + StringBuffer sb = new StringBuffer(); + sb.append("missed values: ["); + for (int i = 0; i < missed.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(missed[i] + ": " + input[missed[i]]); + } + sb.append("]"); + return sb.toString(); + } + + private final ArrayList options = new ArrayList(); + private final boolean stopAtFirstMatch; + private boolean frozen = !FROZEN; + + public Options(boolean stopAtFirstMatch) { + this.stopAtFirstMatch = stopAtFirstMatch; + } + + public void freeze() { + if (frozen != FROZEN) { + frozen = FROZEN; + } + } + + public boolean isFrozen() { + return (frozen == FROZEN); + } + + public void addOption(Option option) { + checkFrozen("adding option", !FROZEN); + LangUtil.throwIaxIfNull(option, "option"); + options.add(option); + } + + /** + * Associate options matched, if any, with input by index. + * If an input element is not matched, the corresponding + * result element will be null. + * If there are multi-argument options matched, then + * only the initial element will be non-null, but it + * will contain the accumulated value of the arguments. + * @param input the String[] of input + * @return Option.Value[] corresponding to input + * @throws Option.InvalidInputException when encountering + * invalid arguments to a matched multi-argument option. + */ + public Values acceptInput(String[] input) + throws Option.InvalidInputException { + checkFrozen("matching options", FROZEN); + if ((null == input) || (0 == input.length)) { + return Values.EMPTY; + } + Option.Value[] results = new Option.Value[input.length]; + for (int i = 0; i < input.length; i++) { + Option.Value result = firstMatch(input[i]); + final int index = i; + if (null != result) { + for (int len = result.option.numArguments(); + len > 0; + len--) { + i++; + if (i >= input.length) { + throw new Option.InvalidInputException( + "not enough arguments", + null, + result.option); + } + result = result.nextInput(input[i]); + } + } + results[index] = result; + } + return Values.wrapValues(results); + } + + private void checkFrozen(String actionLabel, boolean expectFrozen) { + if (expectFrozen != isFrozen()) { + if (null == actionLabel) { + actionLabel = "use"; + } + if (expectFrozen) { + actionLabel = "must freeze before " + actionLabel; + } else { + actionLabel = "frozen before " + actionLabel; + } + throw new IllegalStateException(actionLabel); + } + } + + private Option.Value firstMatch(String value) { + LangUtil.throwIaxIfNull(value, "value"); +// ArrayList list = new ArrayList(); + for (Iterator iter = options.iterator(); iter.hasNext();) { + Option option = (Option) iter.next(); + Option.Value result = option.acceptValue(value); + if (null != result) { + return result; + } + } + return null; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/util/options/OptionsTests.java b/testing/src/test/java/org/aspectj/testing/util/options/OptionsTests.java deleted file mode 100644 index 0b82c7dc9..000000000 --- a/testing/src/test/java/org/aspectj/testing/util/options/OptionsTests.java +++ /dev/null @@ -1,30 +0,0 @@ -/* ******************************************************************* - * Copyright (c) 2003 Contributors. - * All rights reserved. - * This program and the accompanying materials are made available - * under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Wes Isberg initial implementation - * ******************************************************************/ - - -package org.aspectj.testing.util.options; - -import junit.framework.*; - -public class OptionsTests extends TestCase { - - public static Test suite() { - TestSuite suite = new TestSuite(OptionsTests.class.getName()); - //$JUnit-BEGIN$ - suite.addTestSuite(OptionsTest.class); - //$JUnit-END$ - return suite; - } - - public OptionsTests(String name) { super(name); } - -} diff --git a/testing/src/test/java/org/aspectj/testing/util/options/Values.java b/testing/src/test/java/org/aspectj/testing/util/options/Values.java new file mode 100644 index 000000000..574969080 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/util/options/Values.java @@ -0,0 +1,714 @@ +/* ******************************************************************* + * Copyright (c) 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Wes Isberg initial implementation + * ******************************************************************/ + +package org.aspectj.testing.util.options; + +import java.util.*; + +import org.aspectj.testing.util.options.Option.Value; +import org.aspectj.util.LangUtil; + +/** + * Wrapper for Value[] that handles search boilerplate. + */ +public class Values { + public static final Values EMPTY; + /** used by methods taking Selector to halt processing early */ + private static final boolean VERIFYING = true; + private static final boolean FIND_ALL = true; + + private static final String NO_ERROR = "no error"; + + static { + EMPTY = new Values(new Value[0]); + } + + public static Values wrapValues(Value[] values) { + if ((null == values) || (0 == values.length)) { + return EMPTY; + } + return new Values(values); + } + + public static Values wrapValues(Values[] values) { + if ((null == values) || (0 == values.length)) { + return EMPTY; + } + Value[] input = null; + if (values.length == 1) { + input = values[0].asArray(); + LangUtil.throwIaxIfNull(input, "values"); + } else { + int length = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + LangUtil.throwIaxIfNull(null, "null value[" + i + "]"); + } + length += values[i].length(); + } + input = new Value[length]; + length = 0; + Value[] temp; + for (int i = 0; i < values.length; i++) { + temp = values[i].asArray(); + System.arraycopy(temp, 0, input, length, temp.length); + length += temp.length; + } + } + return new Values(input); + } + + static int[] invert(int[] missed, int length) { + final int MAX = length; + final int len = MAX - missed.length; + final int[] result = new int[len]; + int missedIndex = 0; + int resultIndex = 0; + for (int counter = 0; counter < MAX; counter++) { + // catch result up to missed + while (((missedIndex >= missed.length) + || (missed[missedIndex] > counter)) + && (counter < MAX)) { + result[resultIndex++] = counter++; + } + // absorb missed up to counter + while ((missedIndex < missed.length) + && (missed[missedIndex] <= counter) + && (counter < MAX)) { + missedIndex++; + } + } + return result; + } + + private static Option.Value[] toArray(ArrayList list) { + return (Option.Value[]) list.toArray(new Option.Value[0]); + } + + /** + * Resolve input to remove any values matching the same options, + * where option conflicts are handled by option forcing. + * First, for any force-off value, all matching set-on and + * the force-off itself are removed. At this time, if there + * is a matching force-on, then this will return an error. + * Next, for any force-on value, it is converted to set-on, + * and any other matching set-on value is removed. + * Finally, this signals a collision if two values share + * the same option family and the family reports that this is + * a collision. + * In all cases, only the first error detected is reported. + * @param input the Option.Value[] matched from the input, + * (forced/duplicate options will be set to null) + * @return String error during resolution, or null if no error + */ + private static String resolve(Option.Value[] input) { + String err = null; + if (LangUtil.isEmpty(input)) { + return null; + } + + Map familyToMatches = new TreeMap(); + for (int i = 0;(null == err) && (i < input.length); i++) { + if (null != input[i]) { + Option.Family family = input[i].option.getFamily(); + int[] matches = (int[]) familyToMatches.get(family); + if (null == matches) { + matches = match(input, i); + familyToMatches.put(family, matches); + } + } + } + + familyToMatches = Collections.unmodifiableMap(familyToMatches); + for (Iterator iter = familyToMatches.entrySet().iterator(); + (null == err) && iter.hasNext(); + ) { + Map.Entry entry = (Map.Entry) iter.next(); + int[] matches = (int[]) entry.getValue(); + err = resolve(input, matches); + } + return err; + } + + /** + * Resolve all related options into one + * by nullifying or modifying the values. + * + * First, for any force-off value, + * remove all identical set-on + * and the force-off itself, + * and alert on any identical force-on. + * + * Next, for any force-on value, + * convert to set-on, + * throw Error on any same-family force-off value, + * remove any identical force-on or set-on value, + * alert on any other non-identical same-family force-on value, + * remove any same-family set-on value, + * and alert on any same-family set-off value. + * + * Finally, alert if any two remaining values share + * the same option family, unless the option is marked + * as permitting multiple values. + * + * @param input the Option.Value[] matching the input + * @param matches the int[] list of indexes into input for + * values for related by option + * (all such values must have option matched by family) + * @return String error, if any, or null if no error + * @see #match(Option.Value[], int) + */ + private static String resolve(Option.Value[] input, int[] matches) { + String err = null; + // seek force-off +// Option.Value forceOff = null; + Option option = null; + // find and remove any force-off + for (int i = 0;(null == err) && (i < matches.length); i++) { + Option.Value value = input[matches[i]]; + if (null != value) { + // verify that matches are in the same family + if (VERIFYING) { + if (null == option) { + option = value.option; + } else if (!(option.sameOptionFamily(value.option))) { + String s = + value.option + + " has different family from " + + option; + throw new IllegalArgumentException(s); + } + } + if (value.prefix.forceOff()) { + err = removeForceOff(input, value, matches); + } + } + } + // find and set any force-on, removing others + for (int i = 0;(null == err) && (i < matches.length); i++) { + Option.Value value = input[matches[i]]; + if (null != value) { + if (value.prefix.forceOn()) { + err = convertForceOn(input, i, matches); + } + } + } + // remove any exact duplicates + for (int i = 0;(null == err) && (i < matches.length); i++) { + Option.Value value = input[matches[i]]; + if (null != value) { + for (int j = i + 1; j < matches.length; j++) { + if (value.sameValueIdentifier(input[matches[j]])) { + input[matches[j]] = null; + } + } + } + } + // signal error if two left unless permitMultipleFamilyValues + Option.Value first = null; + for (int i = 0;(null == err) && (i < matches.length); i++) { + Option.Value value = input[matches[i]]; + if (null != value) { + if (null == first) { + first = value; + if (first + .option + .getFamily() + .permitMultipleFamilyValues()) { + break; + } + } else { + err = "collision between " + first + " and " + value; + } + } + } + + return err; + } + + /** + * For any force-off value, + * remove all set-on or force-off with same value + * (including the force-off itself), + * and alert on any identical force-on. + * @param input the Option.Value[] matching the input + * @param value the force-off Option.Value to remove + * @param matches the int[] list of indexes into input for + * values for related by option + * (all such values must have matching option) + * @return String error if any + */ + private static String removeForceOff( + Option.Value[] input, + Option.Value value, + int[] matches) { + if (!value.prefix.forceOff()) { + throw new IllegalArgumentException( + "expecting force-off: " + value); + } + for (int i = 0; i < matches.length; i++) { + Option.Value match = input[matches[i]]; + if ((null != match) && value.sameValueIdentifier(match)) { + if (match.prefix.forceOn()) { + return "force conflict between " + + value + + " and " + + match; + } else { + input[matches[i]] = null; // unset matches[i]? + } + } + } + return null; + } + + /** + * For this force-on value, convert to set-on, + * throw Error on any same-family force-off value, + * remove any identical force-on or set-on value, + * alert on any other non-identical same-family force-on value, + * remove any same-family set-on value, + * and alert on any same-family set-off value. + * This must be called after removeForceOff(..). + * @param input the Option.Value[] to modify + * @param valueIndex the int index in matches to find the force-on + * and to start after + * @param matches the int[] map into input entries with matching options + * @return + * @throw Error if any matching force-off found + */ + private static String convertForceOn( + Option.Value[] input, + int valueIndex, + int[] matches) { + Option.Value value = input[matches[valueIndex]]; + if (!value.prefix.forceOn()) { + throw new IllegalArgumentException( + "expecting force-on: " + value); + } + input[matches[valueIndex]] = value.convert(Option.ON); + for (int i = 0; i < matches.length; i++) { + if (i == valueIndex) { + continue; + } + Option.Value match = input[matches[i]]; + if (null != match) { + // assert match.sameOptionFamily(value); + if (match.prefix.forceOff()) { + throw new Error( + "unexpected force-off:" + + match + + " when processing " + + value); + } + if (value.option.sameOptionIdentifier(match.option)) { + input[matches[i]] = null; + // remove any identical force-on or set + } else if (match.prefix.forceOn()) { + return "conflict between " + match + " and " + value; + } else if (match.prefix.isSet()) { + input[matches[i]] = null; + // remove any same-value set-on value + } else { // same family, force-off + return "collision between " + match + " and " + value; + } + } + } + return null; + } + + /** + * Get a list of input matching the option in the initial value, + * rendered as indexes into the input array. + * @param input the Option.Value[] to seek in + * @param start the int index of the starting position + * @return int[] of indexes into input with the same option + * as index[start] - never null, but can be empty + */ + private static int[] match(Option.Value[] input, int start) { + IntList result = new IntList(); + Option.Family key = null; + Option.Family nextKey = null; + for (int i = start; i < input.length; i++) { + if (null != input[i]) { + nextKey = input[i].option.getFamily(); + if (null == key) { + key = nextKey; + result.add(i); + } else if (key.equals(nextKey)) { + result.add(i); + } + } + } + return result.getList(); + } + + static int nullify(Option.Value[] values, Selector selector) { + LangUtil.throwIaxIfNull(selector, "selector"); + int changed = 0; + for (int i = 0; i < values.length; i++) { + final boolean accepted; + try { + accepted = selector.accept(values[i]); + } catch (Error e) { + if (e != Selector.STOP) { + throw e; + } + break; + } + if (accepted) { + if (null != values[i]) { + values[i] = null; + changed++; + } + } + } + return changed; + } + + /** + * Render set values as String using associated prefix. + * @param values the Value[] to render + * @return String[] of values rendered for output + * (never null or longer than values, but might be shorter) + */ + private static String[] render(Value[] values) { + ArrayList list = new ArrayList(); + for (int i = 0; i < values.length; i++) { + if (null != values[i]) { + String[] output = values[i].unflatten(); + if (LangUtil.isEmpty(output)) { + throw new Error("no output for " + values[i]); + } + + String s = values[i].prefix.render(output[0]); + if (null != s) { // this means the prefix is set + list.add(s); + for (int j = 1; j < output.length; j++) { + list.add(output[j]); + } + } + } + } + return (String[]) list.toArray(new String[list.size()]); + } + + private final Option.Value[] values; + private Option.Value[] valuesNotNull; + private String resolveError; + + private Values(Value[] values) { + this.values = new Value[values.length]; + System.arraycopy(values, 0, this.values, 0, values.length); + } + + public int length() { + return values.length; + } + + public Option.Value[] asArray() { + Option.Value[] result = new Option.Value[values.length]; + System.arraycopy(values, 0, result, 0, result.length); + return result; + } + + /** + * Emit as String[] the non-null values. + * @return String[] of matched entries (never null, elements not null) + */ + public String[] render() { + return Values.render(valuesNotNull()); + } + + public String toString() { + return Arrays.asList(values).toString(); + } + + /** + * Create index into values of those that were matched, + * including the options and their arguments. + * @return int[] of elements in values that are not null (options) + * or that represent option arguments + */ + public int[] indexMatches() { + // must be in order, low to high + final int[] missed = indexMissedMatches(); + return invert(missed, length()); + } + + /** + * Create index into values of missed input, + * taking into account that matched arguments are + * represented as null. + * @return int[] of elements in values that are null + * or optionally represent option arguments + */ + public int[] indexMissedMatches() { + MissedSelector selector = new MissedSelector(); + find(selector, FIND_ALL); + String errors = selector.getErrors(); + if (null != errors) { + throw new Error(errors); + } + return selector.getResult(); + } + + public Value firstInFamily(Option.Family family) { + return findFirst(new ValueSelector(family)); + } + + public Value[] allInFamily(Option.Family family) { + return find(new ValueSelector(family), FIND_ALL); + } + + public Value firstOption(Option option) { + return findFirst(new ValueSelector(option)); + } + + public Value[] allOption(Option option) { + return find(new ValueSelector(option), FIND_ALL); + } + + public Value firstValue(Option option, String value) { + LangUtil.throwIaxIfNull(value, "value"); + return findFirst(new ValueSelector(option, value)); + } + + public Value[] allValues(Option option, String value) { + LangUtil.throwIaxIfNull(value, "value"); + return find(new ValueSelector(option, value), FIND_ALL); + } + + public boolean isResolved() { + return ((this != EMPTY) && (null != resolveError)); + } + + /** + * + * @param selector the Selector to pick out entries to nullify + * (should throw STOP to halt processing) + * @return Values resulting from nullifying entries, + * or this if none were changed + */ + public Values nullify(Selector selector) { + if (null == selector) { + return this; + } + Value[] temp = asArray(); + int changed = nullify(temp, selector); + if (0 == changed) { + return this; + } + return new Values(temp); + } + + /** + * Resolve options, removing duplicates by force if necessary. + * If any error is returned, then the values are left unchanged. + * @return String error, if any + * @throws IllegalStateException if isResolved() + */ + public String resolve() { + if (isResolved()) { + throw new IllegalStateException("already resolved"); + } + Option.Value[] temp = asArray(); + resolveError = resolve(temp); + if (null == resolveError) { + System.arraycopy(temp, 0, values, 0, temp.length); + valuesNotNull = null; + resolveError = NO_ERROR; + return null; + } + return resolveError; + } + + protected Option.Value findFirst(Selector filter) { + Option.Value[] result = find(filter, !FIND_ALL); + return (0 == result.length ? null : result[0]); + } + + protected Option.Value[] find(Selector filter, boolean findAll) { + LangUtil.throwIaxIfNull(filter, "filter"); + ArrayList result = new ArrayList(); + for (int i = 0; i < values.length; i++) { + final boolean accepted; + try { + accepted = filter.accept(values[i]); + } catch (Error e) { + if (Selector.STOP != e) { + throw e; + } + break; + } + if (accepted) { + result.add(values[i]); + if (findAll != FIND_ALL) { + break; + } + } + } + return toArray(result); + } + + private Option.Value[] valuesNotNull() { + if (null == valuesNotNull) { + ArrayList list = new ArrayList(); + for (int i = 0; i < this.values.length; i++) { + if (null != this.values[i]) { + list.add(this.values[i]); + } + } + valuesNotNull = toArray(list); + } + return valuesNotNull; + } + + public static class Selector { + public static final Error STOP = new Error("stop invoking Selector"); + protected Selector() { + } + protected boolean accept(Value value) { + return false; + } + } + protected static class ValueSelector extends Selector { + + private final Option option; + private final Option.Family family; + private final String value; + ValueSelector(Option.Family family) { + LangUtil.throwIaxIfNull(family, "family"); + this.family = family; + option = null; + value = null; + } + ValueSelector(Option option) { + this(option, (String) null); + } + ValueSelector(Option option, String value) { + LangUtil.throwIaxIfNull(option, "option"); + this.option = option; + family = null; + this.value = value; + } + protected boolean accept(Value value) { + if (null == value) { + return false; + } + if (null != family) { + return family.sameFamily(value.option.getFamily()); + } else if (!option.sameOptionIdentifier(value.option)) { + return false; + } else { + return ((null == this.value) + || (this.value.equals(value.value))); + } + } + } + + /** pick all null entries (except for args), return as int[] */ + protected static class MissedSelector extends Selector { + public static final String DELIM = "; "; + final IntList result = new IntList(); + int index; + final StringBuffer errors = new StringBuffer(); + int argsExpected; + Option argsExpectedFor; + MissedSelector() { + } + + int[] getResult() { + return result.getList(); + } + + /** + * add index if value is null + * unless skipArguments + */ + protected boolean accept(Value value) { + index++; + if (null != value) { + if (0 < argsExpected) { // expected more (null) args + missedArgsFor(argsExpectedFor, argsExpected); + } + argsExpected = value.option.numArguments(); + argsExpectedFor = value.option; + } else if (0 < argsExpected) { // ignore null in arg position + argsExpected--; + if (0 == argsExpected) { + argsExpectedFor = null; + } + } else { // null, not expecting arg, so missing + result.add(index - 1); + return true; + } + return false; + } + + private void missedArgsFor(Option option, int numArgs) { + errors.append("missed "); + errors.append(numArgs + " args for "); + errors.append(option + DELIM); + } + + String getErrors() { + if (0 < argsExpected) { + } + if (0 == errors.length()) { + return null; + } + return errors.toString(); + } + } + + static class IntList { + // not synchronized - used only in one thread + static String render(int[] input) { + if (null == input) { + return "null"; + } + StringBuffer sb = new StringBuffer(); + sb.append("["); + for (int i = 0; i < input.length; i++) { + if (i > 0) { + sb.append(", " + input[i]); + } else { + sb.append("" + input[i]); + } + } + sb.append("]"); + return sb.toString(); + } + + private int[] input = new int[256]; + private int insert; + private void add(int i) { + if (insert >= input.length) { + int[] temp = new int[insert + 256]; + for (int j = 0; j < input.length; j++) { + temp[j] = input[j]; + } + input = temp; + } + input[insert++] = i; + } + + private int[] getList() { + int[] result = new int[insert]; + for (int i = 0; i < result.length; i++) { + result[i] = input[i]; + } + return result; + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/xml/AjcSpecXmlReader.java b/testing/src/test/java/org/aspectj/testing/xml/AjcSpecXmlReader.java new file mode 100644 index 000000000..370a9ac91 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/xml/AjcSpecXmlReader.java @@ -0,0 +1,526 @@ +/* ******************************************************************* + * Copyright (c) 1999-2001 Xerox Corporation, + * 2002 Palo Alto Research Center, Incorporated (PARC), + * 2003 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * Wes Isberg resolver + * ******************************************************************/ + +package org.aspectj.testing.xml; + +import java.io.*; +//import java.util.Vector; + +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.bridge.SourceLocation; +import org.aspectj.testing.harness.bridge.*; +import org.aspectj.testing.util.RunUtils; +import org.aspectj.util.LangUtil; +import org.xml.sax.*; +import org.xml.sax.SAXException; + +/** + * 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) + * + * Debugging + * - use logLevel = 2 for tracing + * - common mistakes + * - dtd has to match input + * - no rule defined (or misdefined) so element ignored + * - property read-only (?) + */ + +// 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 = ""; + + 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("../tests/ajcTestSuite2.dtd")); + } + + /** + * Write a DTD to dtdFile. + * @deprecated + * @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(""); + //out.println(getDocType()); + } finally { + out.close(); + } + } + + private static final String[] LOG = new String[] {"info", "debug", "trace" }; + + // XXX logLevel n>0 causes JUnit tests to fail! + private int logLevel = 0; // use 2 for tracing + + 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 = makeDigester(file); + 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 = file.getAbsoluteFile(); + result.setSourceLocation(new SourceLocation(file, 1)); + File suiteDir = file.getParentFile(); + if (null == suiteDir) { + // should not be the case if absolute + suiteDir = new File("."); // user.dir? + } + result.setSuiteDirFile(suiteDir); + if (result.runtime.isVerbose()) { // XXX hack fixup + RunUtils.enableVerbose(result); + } + } + return result; + } + + private Digester makeDigester(final File suiteFile) { + // implement EntityResolver directly; set is failing + Digester result = new Digester() { + final SuiteResolver resolver = new SuiteResolver(suiteFile); + public InputSource resolveEntity( + String publicId, + String systemId) + throws SAXException { + return resolver.resolveEntity(publicId, systemId); + } + }; + setupDigester(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); +// try { +// // this is the correct approach, but the commons parser +// // fails to accept a second, overriding registration - see +// // http://lists.xml.org/archives/xml-dev/200111/msg00959.html +// digester.getXMLReader().setEntityResolver(new SuiteResolver(suiteFile)); +// } catch (SAXException e) { +// System.err.println("unable to set entity resolver"); +// e.printStackTrace(System.err); +// } + + // 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 + "/" +SoftSourceLocation.XMLNAME; + + // ---- 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, + new String[] { "class", "vm", "skipTester", "fork", "vmargs", "aspectpath", "module"}, + new String[] { "className", "javaVersion", "skipTester", "fork", "vmArgs", "aspectpath", "module"}); + digester.addSetProperties(dirchangesX); + digester.addSetProperties(messageX); + digester.addSetProperties(messageSrcLocX, "line", "lineAsString"); + digester.addSetProperties(messageX, "kind", "kindAsString"); + digester.addSetProperties(messageX, "line", "lineAsString"); + //digester.addSetProperties(messageX, "details", "details"); + // 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()); + // setSourceLocation is for the inline variant + // addSourceLocation is for the extra + digester.addSetNext(messageSrcLocX, "addSourceLocation", 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", + "staging", "badInput", "reuseCompiler", "includeClassesDir", + "argfiles", "aspectpath", "classpath", "extdirs", + "sourceroots", "xlintfile", "outjar"}), + new BProps(IncCompilerRun.Spec.class, + new String[] { "tag" }), + new BProps(JavaRun.Spec.class, + new String[] { "className", "skipTester", "options", + "javaVersion", "errStreamIsError", "outStreamIsError", + "fork", "vmArgs", "aspectpath"}), + // 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", "details", "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(); + Sandbox sandbox = null; + Validator validator = null; +// AjcTest ajctest = new AjcTest(test, sandbox, validator); +// ajctest.addRunSpec((AbstractRunSpec) null); +//// test.makeIncCompilerRun((IncCompilerRun.Spec) null); +//// test.makeJavaRun((JavaRun.Spec) null); +// ajctest.setDescription((String) null); +// ajctest.setTestBaseDirOffset((String) null); +// ajctest.setBugId((String) null); +// ajctest.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); + crunSpec.setIncludeClassesDir(false); + crunSpec.setReuseCompiler(false); + crunSpec.setXlintfile((String) null); + crunSpec.setOutjar((String)null); + + IncCompilerRun.Spec icrunSpec = new IncCompilerRun.Spec(); + icrunSpec.addMessage((IMessage) null); + icrunSpec.setTag((String) null); + icrunSpec.setFresh(false); + + 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); + jrunspec.setErrStreamIsError("false"); + jrunspec.setOutStreamIsError("false"); + jrunspec.setAspectpath(""); + jrunspec.setClasspath(""); + jrunspec.setFork(false); + jrunspec.setLTW("false"); + jrunspec.setException("Error"); + + + 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); + m.setDetails((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; + } + } + + /** + * Find file NAME=="ajcTestSuite.dtd" from some reasonably-local + * relative directories. + * XXX bug: commons parser doesn't accept second registration, + * so we override Digester's implementation instead. + * XXX cannot JUnit test SuiteResolver since they run from + * local directory with valid reference + * XXX does not fix JDK 1.4 parser message "unable to resolve without base URI" + * XXX should be able to just set BaseURI instead... + */ + static class SuiteResolver implements EntityResolver { + public static final String NAME = "ajcTestSuite.dtd"; + private static boolean isDir(File dir) { + return ((null != dir) && dir.canRead() && dir.isDirectory()); + } + private final File suiteFile; + public SuiteResolver(File suiteFile) { + this.suiteFile = suiteFile; + } + + + private String getPath(String id) { + // first, try id relative to suite file + final File suiteFileDir = suiteFile.getParentFile(); + if (isDir(suiteFileDir)) { + String path = suiteFileDir.getPath() + + "/" + id; + File result = new File(path); + if (result.canRead() && result.isFile()) { + return result.getPath(); + } + } + // then try misc paths relative to suite file or current dir + final File[] baseDirs = new File[] + { suiteFileDir, new File(".") + }; + final String[] locations = new String[] + { ".", "..", "../tests", "../../tests", + "../../../tests", "tests", "modules/tests" + }; + File baseDir; + for (int j = 0; j < baseDirs.length; j++) { + baseDir = baseDirs[j]; + if (!isDir(baseDir)) { + continue; + } + for (int i = 0; i < locations.length; i++) { + File dir = new File(baseDir, locations[i]); + if (isDir(dir)) { + File temp = new File(dir, NAME); + if (temp.isFile() && temp.canRead()) { + return temp.getPath(); + } + } + } + } + return null; + } + public InputSource resolveEntity( + String publicId, + String systemId) + throws SAXException { + InputSource result = null; + if ((null != systemId) && + systemId.endsWith(NAME)) { + String path = getPath(systemId); + if (null != path) { + result = new InputSource(path); + result.setSystemId(path); + result.setPublicId(path); + } + } + return result; + } + } +} + +//private String getDocTypePath() { +// String result = null; +// if (null != suiteFile) { +// FileReader fr = null; +// try { +// fr = new FileReader(suiteFile); +// BufferedReader reader = new BufferedReader(fr); +// String line; +// while (null != (line = reader.readLine())) { +// String upper = line.trim(); +// line = upper.toLowerCase(); +// if (line.startsWith("<")) { +// if (line.startsWith(""); + String name = SoftMessage.XMLNAME; + r.append(EOL + " "); + r.append(EOL + " "); + r.append(EOL + " "); + name = SoftSourceLocation.XMLNAME; + r.append(EOL + " "); + r.append(EOL + " "); + r.append(EOL + " "); + r.append(EOL + " "); + r.append(EOL + " "); + + r.append(EOL + "] >"); + INLINE_DOCTYPE = r.toString(); + } + + private static final String[] LOG = new String[] {"info", "debug", "trace" }; + + private int logLevel; + + /** + * Print IMessage[] to the output file as XML. + * @param output the File to write to - overwritten + * @param messages the IMessage[] to write + * @return null if no warnings detected, warnings otherwise + */ + public String writeMessages(File output, IMessage[] messages) throws IOException { + LangUtil.throwIaxIfNull(output, "output"); + LangUtil.throwIaxIfFalse(!LangUtil.isEmpty(messages), "no messages"); + PrintWriter writer = new PrintWriter(new FileOutputStream(output)); + XMLWriter printSink = new XMLWriter(writer); + writer.println(""); + writer.println(INLINE_DOCTYPE); + writer.println(""); + writer.println("<" + MessageList.XMLNAME + ">"); + SoftMessage.writeXml(printSink, messages); + writer.println(""); + writer.close(); + return null; + } + + /** @param level 0..2, info..trace */ + public void setLogLevel(int level) { + if (level < 0) { + level = 0; + } + if (level > 2) { + level = 2; + } + logLevel = level; + } + + /** + * Read the specifications for a list of IMessage from an XML file. + * @param file the File must be readable, comply with DOCTYPE. + * @return IMessage[] read from file + * @see setLogLevel(int) + */ + public IMessage[] readMessages(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); + + MessageListHolder holder = new MessageListHolder(); + 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; + } + } + return (null == holder.list + ? new IMessage[0] + : holder.list.getMessages()); + } + + /** 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 messageListX = MessageList.XMLNAME; + final String messageX = messageListX + "/" + SoftMessage.XMLNAME; + final String messageSrcLocX = messageX + "/" + SoftSourceLocation.XMLNAME; + + // ---- each sub-element needs to be created + // handle messages the same at any level + digester.addObjectCreate(messageListX, MessageList.class.getName()); + digester.addObjectCreate(messageX, SoftMessage.class.getName()); + digester.addObjectCreate(messageSrcLocX, SoftSourceLocation.class.getName()); + + // ---- set bean properties for sub-elements created automatically + // -- some remapped - warnings + // - if property exists, map will not be used + digester.addSetProperties(messageListX); + digester.addSetProperties(messageX); + digester.addSetProperties(messageSrcLocX); + digester.addSetProperties(messageX, "kind", "kindAsString"); + digester.addSetProperties(messageX, "line", "lineAsString"); + + // ---- when subelements are created, add to parent + digester.addSetNext(messageListX, "addMessage", IMessage.class.getName()); + digester.addSetNext(messageX, "setSourceLocation", ISourceLocation.class.getName()); + + // can set parent, but prefer to have "knows-about" flow down only... + } + + // ------------------------------------------------------------ testing code + + /** + * 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"); } +// +// MessageListHolder holder = new MessageListHolder(); +// MessageList ml = new MessageList(); +// SoftMessage m = new SoftMessage(); +// SoftSourceLocation sl = new SoftSourceLocation(); +// +// holder.setMessageList(ml); +// ml.addMessage((IMessage) null); +// m.setSourceLocation(sl); +// m.setText((String) null); +// m.setKindAsString((String) null); +// +// sl.setFile((String) null); +// sl.setLine((String) null); +// sl.setColumn((String) null); +// sl.setEndLine((String) null); +// +// // add attribute setters to validate? +// } + + // inner classes, to make public for bean utilities + /** a list of messages */ + public static class MessageList { + public static final String XMLNAME = "message-list"; + private List messages = new ArrayList(); + public void addMessage(IMessage message) { + messages.add(message); + } + IMessage[] getMessages() { + return (IMessage[]) messages.toArray(new IMessage[0]); + } + } + + /** push on digester stack to hold message list */ + public static class MessageListHolder { + public MessageList list; + public void setMessageList(MessageList list) { + this.list = list; + } + } + +} + + + diff --git a/testing/src/test/java/org/aspectj/testing/xml/SoftMessage.java b/testing/src/test/java/org/aspectj/testing/xml/SoftMessage.java new file mode 100644 index 000000000..f59bf397e --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/xml/SoftMessage.java @@ -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 Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.xml; + +import java.io.File; +import java.util.ArrayList; +//import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.aspectj.bridge.*; +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; + +/** + * Implement messages. + * This implementation is immutable if ISourceLocation is immutable, + * except for adding source locations. + */ +public class SoftMessage implements IMessage { + 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 String details; + private int id; + private int sourceStart,sourceEnd; + private final ArrayList extraSourceLocations = new ArrayList(); + + //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, IMessageHolder messages) { + if ((null == out) + || (null == messages) + || (0 == messages.numMessages(null, true))) { + return; + } + List list = messages.getUnmodifiableListView(); + for (Iterator iter = list.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 + * XXX has to sync with ajcTests.dtd + * @throws IllegalArgumentException if message.getThrown() is not null + */ + public static void writeXml( + XMLWriter out, + IMessage message) { // XXX short form only, no files + if ((null == out) || (null == message)) { + return; + } + Throwable thrown = message.getThrown(); + if (null != thrown) { + String m = "unable to write " + message + " thrown not permitted"; + throw new IllegalArgumentException(m); + } + final String elementName = XMLNAME; + out.startElement(elementName, false); + out.printAttribute("kind", message.getKind().toString()); + String value = message.getMessage(); + if (null != value) { + value = XMLWriter.attributeValue(value); + out.printAttribute("text", value); + } + value = message.getDetails(); + if (null != value) { + value = XMLWriter.attributeValue(value); + out.printAttribute("details", value); + } + ISourceLocation sl = message.getSourceLocation(); + if (null != sl) { + int line = sl.getLine(); + if (-1 < line) { + out.printAttribute("line", "" + line); + } + File file = sl.getSourceFile(); + if ((null != file) && !ISourceLocation.NO_FILE.equals(file)) { + value = XMLWriter.attributeValue(file.getPath()); + out.printAttribute("file", value); + } + } + List extras = message.getExtraSourceLocations(); + if (!LangUtil.isEmpty(extras)) { + out.endAttributes(); + for (Iterator iter = extras.iterator(); iter.hasNext();) { + /*ISourceLocation element = (ISourceLocation)*/ iter.next(); + SoftSourceLocation.writeXml(out, sl); + } + } + 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; + } + + public boolean isTaskTag() { + return kind == IMessage.TASKTAG; + } + + /** @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; + } + + public boolean getDeclared() { return false; } + + /** @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 getSourceLocation() { + 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(null == getKind() ? "" : getKind().toString()); + + String messageString = getMessage(); + if (!LangUtil.isEmpty(messageString)) { + result.append(messageString); + } + + ISourceLocation loc = getSourceLocation(); + if ((null != loc) && (loc != ISourceLocation.NO_FILE)) { + result.append(" at " + loc); + } + if (null != thrown) { + result.append(" -- " + LangUtil.renderExceptionShort(thrown)); + } + return result.toString(); + } + + public String getDetails() { + return details; + } + + public void setDetails(String string) { + details = string; + } + + public int getID() { + return id; + } + + public void setID(int id) { + this.id = id; + } + + public int getSourceStart() { + return sourceStart; + } + + public void setSourceStart(int s) { + sourceStart = s; + } + + public int getSourceEnd() { + return sourceStart; + } + + public void setSourceEnd(int s) { + sourceEnd = s; + } + + /* (non-Javadoc) + * @see org.aspectj.bridge.IMessage#getExtraSourceLocations() + */ + public List getExtraSourceLocations() { + return extraSourceLocations; + } + public void addSourceLocation(ISourceLocation location) { + if (null != location) { + extraSourceLocations.add(location); + } + } +} diff --git a/testing/src/test/java/org/aspectj/testing/xml/SoftSourceLocation.java b/testing/src/test/java/org/aspectj/testing/xml/SoftSourceLocation.java new file mode 100644 index 000000000..17f5ae673 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/xml/SoftSourceLocation.java @@ -0,0 +1,133 @@ +/* ******************************************************************* + * Copyright (c) 1999-2001 Xerox Corporation, + * 2002 Palo Alto Research Center, Incorporated (PARC), + * 2004 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * Wes Isberg 2004 updates + * ******************************************************************/ + +package org.aspectj.testing.xml; + + +import java.io.File; + +import org.aspectj.bridge.ISourceLocation; +import org.aspectj.util.LangUtil; + +/** + * A mutable ISourceLocation for XML initialization of tests. + * This does not support reading/writing of the attributes + * column, context, or endline. + */ +public class SoftSourceLocation implements ISourceLocation { + public static final File NONE = ISourceLocation.NO_FILE; + public static final String XMLNAME = "source"; + + /** + * Write an ISourceLocation as XML element to an XMLWriter sink. + * @param out the XMLWriter sink + * @param sl the ISourceLocation to write. + */ + public static void writeXml(XMLWriter out, ISourceLocation sl) { + if ((null == out) || (null == sl)) { + return; + } + final String elementName = XMLNAME; + out.startElement(elementName, false); + out.printAttribute("line", "" + sl.getLine()); + // other attributes not supported + File file = sl.getSourceFile(); + if ((null != file) && !ISourceLocation.NO_FILE.equals(file)) { + String value = XMLWriter.attributeValue(file.getPath()); + out.printAttribute("file", value); + } + out.endElement(elementName); + } + + private File sourceFile; + private int line = -1; // required for no-line comparisons to work + private int column; + private int endLine; + private String context; + + 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 String getContext() { + return context; + } + + public void setFile(String sourceFile) { + this.sourceFile = new File(sourceFile); + } + + public void setLine(String line) { + setLineAsString(line); + } + + public void setLineAsString(String line) { + this.line = convert(line); + if (0 == endLine) { + endLine = this.line; + } + } + public String getLineAsString() { + return ""+line; + } + + public void setColumn(String column) { + this.column = convert(column); + } + + public void setEndLine(String line) { + this.endLine = convert(line); + } + + public void setContext(String context) { + this.context = context; + } + + private int convert(String in) { + return Integer.valueOf(in).intValue(); + } + + public String getLocationContext() { + return null; + } + + public int getOffset() { + return -1; + } + + /** @return String : {context\n}file:line:column */ + public String toString() { + return (null == context ? "" : context + LangUtil.EOL) + + getSourceFile().getPath() + + ":" + getLine() ; + } + + public String getSourceFileName() { + return null; + } +} diff --git a/testing/src/test/java/org/aspectj/testing/xml/TestingXmlTests.java b/testing/src/test/java/org/aspectj/testing/xml/TestingXmlTests.java deleted file mode 100644 index ff8093c32..000000000 --- a/testing/src/test/java/org/aspectj/testing/xml/TestingXmlTests.java +++ /dev/null @@ -1,35 +0,0 @@ -/* ******************************************************************* - * 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 v1.0 - * which accompanies this distribution and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Xerox/PARC initial implementation - * ******************************************************************/ - - -package org.aspectj.testing.xml; - -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; - -public class TestingXmlTests extends TestCase { - - public static Test suite() { - TestSuite suite = new TestSuite(TestingXmlTests.class.getName()); - //$JUnit-BEGIN$ - suite.addTestSuite(AjcSpecXmlReaderTest.class); - suite.addTestSuite(MessageListXmlReaderTest.class); - suite.addTestSuite(XMLWriterTest.class); - //$JUnit-END$ - return suite; - } - - public TestingXmlTests(String name) { super(name); } - -} diff --git a/testing/src/test/java/org/aspectj/testing/xml/XMLWriter.java b/testing/src/test/java/org/aspectj/testing/xml/XMLWriter.java new file mode 100644 index 000000000..422576c56 --- /dev/null +++ b/testing/src/test/java/org/aspectj/testing/xml/XMLWriter.java @@ -0,0 +1,365 @@ +/* ******************************************************************* + * 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 v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Xerox/PARC initial implementation + * ******************************************************************/ + +package org.aspectj.testing.xml; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Stack; + +import org.aspectj.util.LangUtil; + +/** + * 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; + + /** extremely inefficient! */ + public static String attributeValue(String input) { +// if (-1 != input.indexOf("&")) { +// String saved = input; +// input = LangUtil.replace(input, "&", "ampamp;"); +// if (-1 == input.indexOf("&")) { +// input = saved; +// } else { +// input = LangUtil.replace(input, "&", "&"); +// input = LangUtil.replace(input, "ampamp;", "&"); +// } +// } else if (-1 != input.indexOf("&")) { +// input = LangUtil.replace(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 + ""); + } 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 + ""); + } + + 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; + } + } + +} -- cgit v1.2.3