From 50567fc1b7174aa7624c7bbbaf8457fc046b7241 Mon Sep 17 00:00:00 2001 From: wisberg Date: Fri, 9 Jan 2004 07:23:35 +0000 Subject: [PATCH] harness support for message details, extra source locations Completely new (clearer?) message-checking code. --- .../harness/bridge/AjcMessageHandler.java | 574 ++++++++-------- .../harness/bridge/FlatSuiteReader.java | 3 +- .../src/org/aspectj/testing/util/Diffs.java | 612 +++++++++++++++--- .../org/aspectj/testing/util/FileUtil.java | 2 +- .../org/aspectj/testing/util/LangUtil.java | 3 +- .../org/aspectj/testing/util/TestDiffs.java | 4 +- .../aspectj/testing/xml/AjcSpecXmlReader.java | 100 +-- .../org/aspectj/testing/xml/SoftMessage.java | 536 ++++++++------- .../testing/xml/SoftSourceLocation.java | 39 +- 9 files changed, 1136 insertions(+), 737 deletions(-) diff --git a/testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java b/testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java index d01f26ddc..dc19ea2b9 100644 --- a/testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java +++ b/testing/src/org/aspectj/testing/harness/bridge/AjcMessageHandler.java @@ -30,298 +30,314 @@ 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 { +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; + /** Comparator for enclosed IMessage diffs */ + public static final Comparator COMP_IMessage = + BridgeUtil.Comparators.MEDIUM_IMessage; - /** 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; + /** Comparator for enclosed File diffs */ + public static final Comparator COMP_File = BridgeUtil.Comparators.WEAK_File; - /** true if there were no error or worse messages expected */ - private final boolean expectingCommandTrue; + /** unmodifiable list of IMessage messages of any type */ + private final List expectedMessagesAsList; - /** 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; - } + /** IMessageHolder variant of expectedMessagesAsList */ + private final IMessageHolder expectedMessages; - /** - * 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 - Diffs messages = new Diffs( - "message", - expected, - actual, - COMP_IMessage, - Diffs.ACCEPT_ALL, - CompilerDiffs.SKIP_UNEXPECTED_INFO); - Diffs recompiled = new Diffs( - "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); - } + /** number of messages FAIL or worse */ + private final int numExpectedFailed; - 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 - } + /** true if there were no error or worse messages expected */ + private final boolean expectingCommandTrue; - /** - * 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; - } + /** unmodifiable list of File expected to be recompiled */ + private final List expectedRecompiled; + // Unused now, but reinstate when supported - 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]); + /** 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); } - } - result = (IMessage[]) list.toArray(new IMessage[0]); - return result; - } + Diffs recompiled = + Diffs.makeDiffs( + "recompiled", + expectedRecompiled, + actualRecompiled, + COMP_File); + diffs = new CompilerDiffs(messages, recompiled); + } + return diffs; + } - - /** - * @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); - } - } + /** + * 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/org/aspectj/testing/harness/bridge/FlatSuiteReader.java b/testing/src/org/aspectj/testing/harness/bridge/FlatSuiteReader.java index 96e15bdc9..cdfe4d542 100644 --- a/testing/src/org/aspectj/testing/harness/bridge/FlatSuiteReader.java +++ b/testing/src/org/aspectj/testing/harness/bridge/FlatSuiteReader.java @@ -24,6 +24,7 @@ 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; @@ -368,7 +369,7 @@ public class FlatSuiteReader implements SFileReader.Maker { abortOnError, System.err); } catch (IOException e) { - IMessage m = Message.fail("reading " + suiteFile, e); + IMessage m = MessageUtil.fail("reading " + suiteFile, e); throw new AbortException(m); } diff --git a/testing/src/org/aspectj/testing/util/Diffs.java b/testing/src/org/aspectj/testing/util/Diffs.java index 4e493534f..4c2ff3d39 100644 --- a/testing/src/org/aspectj/testing/util/Diffs.java +++ b/testing/src/org/aspectj/testing/util/Diffs.java @@ -13,6 +13,7 @@ package org.aspectj.testing.util; +import java.io.File; import java.util.*; import java.util.ArrayList; import java.util.Collections; @@ -22,120 +23,525 @@ 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 */ +/** + * Result struct for expected/actual diffs for Collection + */ public class Diffs { - 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); - - /** name of the thing being diffed - used only for reporting */ - public final String label; - - /** immutable List */ - public final List missing; + /** + * 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); + } + }; - /** immutable List */ - public final List unexpected; + /** + * 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); - /** true if there are any missing or unexpected */ - public final boolean different; - - private Diffs(String label, List missing, List unexpected) { - this.label = label; - this.missing = missing; - this.unexpected = unexpected; - different = ((0 != this.missing.size()) - || (0 != this.unexpected.size())); - } + public static Diffs makeDiffs( + String label, + List expected, + List actual, + Comparator comparator) { + return makeDiffs( + label, + expected, + actual, + comparator, + ACCEPT_ALL, + ACCEPT_ALL); + } - public Diffs( - String label, - List expected, - List actual, - Comparator comparator) { - this(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); + } - public Diffs( - String label, - List expected, - List actual, - Comparator comparator, - Filter missingFilter, - Filter unexpectedFilter) { - label = label.trim(); - if (null == label) { - label = ": "; - } else if (!label.endsWith(":")) { - label += ": "; - } - this.label = " " + label; - ArrayList miss = new ArrayList(); - ArrayList unexpect = new ArrayList(); - - org.aspectj.testing.util.LangUtil.makeSoftDiffs(expected, actual, miss, unexpect, comparator); - if (null != missingFilter) { - for (ListIterator iter = miss.listIterator(); iter.hasNext();) { - if (!missingFilter.accept(iter.next())) { - iter.remove(); - } - } - } - missing = Collections.unmodifiableList(miss); - if (null != unexpectedFilter) { - for (ListIterator iter = unexpect.listIterator(); iter.hasNext();) { - if (!unexpectedFilter.accept(iter.next())) { - iter.remove(); - } - } + private static int getLine(ISourceLocation loc) { + int result = -1; + if (null != loc) { + result = loc.getLine(); } - unexpected = Collections.unmodifiableList(unexpect); - different = ((0 != this.missing.size()) - || (0 != this.unexpected.size())); + return result; } - - /** - * 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); - } + private static int getLine(IMessage message) { + int result = -1; + if ((null != message)) { + result = getLine(message.getSourceLocation()); } + return result; } - - /** @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); + + 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/org/aspectj/testing/util/FileUtil.java b/testing/src/org/aspectj/testing/util/FileUtil.java
index e3ca612d1..b68d7f8b2 100644
--- a/testing/src/org/aspectj/testing/util/FileUtil.java
+++ b/testing/src/org/aspectj/testing/util/FileUtil.java
@@ -193,7 +193,7 @@ public class FileUtil {
         unexp.addAll(Arrays.asList(dir.listFiles(touchedCollector)));
         
         // report any unexpected changes
-        return new Diffs(label, expected, unexp, String.CASE_INSENSITIVE_ORDER);
+        return Diffs.makeDiffs(label, expected, unexp, String.CASE_INSENSITIVE_ORDER);
     }
 
 
diff --git a/testing/src/org/aspectj/testing/util/LangUtil.java b/testing/src/org/aspectj/testing/util/LangUtil.java
index d05fd2edc..c6b4b9ae1 100644
--- a/testing/src/org/aspectj/testing/util/LangUtil.java
+++ b/testing/src/org/aspectj/testing/util/LangUtil.java
@@ -270,8 +270,7 @@ public class LangUtil {
         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.
diff --git a/testing/src/org/aspectj/testing/util/TestDiffs.java b/testing/src/org/aspectj/testing/util/TestDiffs.java
index f07ed9664..2a69b1634 100644
--- a/testing/src/org/aspectj/testing/util/TestDiffs.java
+++ b/testing/src/org/aspectj/testing/util/TestDiffs.java
@@ -80,13 +80,13 @@ public class TestDiffs { // XXX pretty dumb implementation
             reading = actual;
             act = TestDiffs.readTestResults(actual, actual.getPath());
             
-            Diffs tests = new Diffs("tests", exp, act, TestResult.BY_NAME);
+            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 = new Diffs("results", expResults, actResults, TestResult.BY_PASSNAME);
+            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
diff --git a/testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java b/testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java
index 849c59096..60be24296 100644
--- a/testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java
+++ b/testing/src/org/aspectj/testing/xml/AjcSpecXmlReader.java
@@ -52,6 +52,13 @@ public class AjcSpecXmlReader {
      * - 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";
@@ -63,82 +70,6 @@ public class AjcSpecXmlReader {
     public static final String DOCTYPE = "";
 
-    /** xml leader */
-    public static final String FILE_LEADER
-        = "";
-
-    /** 
-     * @deprecated
-     * @return a String suitable as an inlined DOCTYPE statement 
-     */
-    public static String inlineDocType() {
-        return "";
-    }
-
-    /** 
-     * @deprecated
-     * @return the elements of a document type as a String,
-     * using EOL as a line delimiter
-     */
-    public static String getDocType() {
-        if (true) {
-            throw new Error("XXX using ajcTestSuite.dtd");
-        }
-        StringBuffer r = new StringBuffer();
-        final String suiteX = AjcTest.Suite.Spec.XMLNAME;
-        final String ajctestX = AjcTest.Spec.XMLNAME;
-        final String compileX = CompilerRun.Spec.XMLNAME;
-        final String inccompileX = IncCompilerRun.Spec.XMLNAME;
-        final String runX = JavaRun.Spec.XMLNAME;
-        final String dirchangesX = DirChanges.Spec.XMLNAME;
-        final String messageX = SoftMessage.XMLNAME;
-
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   "); // deprecate file?
-        r.append(EOL + "   ");      // if precursor to incremental
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   ");  // add file* if not deprecated
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   ");                // deprecate?
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");  // but Message requires non-null...
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "   ");
-        r.append(EOL + "");
-        return r.toString();
-    }
-
     private static final AjcSpecXmlReader ME 
         = new AjcSpecXmlReader();
     
@@ -162,7 +93,7 @@ public class AjcSpecXmlReader {
         try {
             out.println("");
-            out.println(getDocType());
+            //out.println(getDocType());
         } finally {
             out.close();
         }        
@@ -170,7 +101,8 @@ public class AjcSpecXmlReader {
 
     private static final String[] LOG = new String[] {"info", "debug", "trace" };
     
-    private int logLevel;
+    // XXX logLevel n>0 causes JUnit tests to fail!
+    private int logLevel = 0; // use 2 for tracing 
     
     private AjcSpecXmlReader() {}
 
@@ -292,7 +224,7 @@ public class AjcSpecXmlReader {
         final String runX = ajctestX + "/" + JavaRun.Spec.XMLNAME;
         final String dirchangesX = "*/" + DirChanges.Spec.XMLNAME;
         final String messageX = "*/" + SoftMessage.XMLNAME;
-        final String messageSrcLocX = messageX + "/source-location";
+        final String messageSrcLocX = messageX + "/" +SoftSourceLocation.XMLNAME;
 
         // ---- each sub-element needs to be created
         // handle messages the same at any level
@@ -323,9 +255,10 @@ public class AjcSpecXmlReader {
             new String[] { "className", "javaVersion", "skipTester"});
         digester.addSetProperties(dirchangesX);
         digester.addSetProperties(messageX);
-        digester.addSetProperties(messageSrcLocX);
+        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);
 
@@ -340,7 +273,9 @@ public class AjcSpecXmlReader {
         digester.addSetNext(runX,                 "addChild", JavaRun.Spec.class.getName());
         digester.addSetNext(compileX + "/file",   "addWrapFile", AbstractRunSpec.WrapFile.class.getName());
         digester.addSetNext(messageX,             "addMessage", IMessage.class.getName());
-        digester.addSetNext(messageSrcLocX,       "setSourceLocation", ISourceLocation.class.getName());
+        // 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...
@@ -372,7 +307,7 @@ public class AjcSpecXmlReader {
                 new BProps(AbstractRunSpec.WrapFile.class, 
                     new String[] { "path"}),
                 new BProps(SoftMessage.class, 
-                    new String[] { "kindAsString", "lineAsString", "text", "file"})
+                    new String[] { "kindAsString", "lineAsString", "text", "details", "file"})
                     // mapped from { "kind", "line", ...}
             };
     }
@@ -430,6 +365,7 @@ public class AjcSpecXmlReader {
         m.setSourceLocation((ISourceLocation) null);
         m.setText((String) null);
         m.setKindAsString((String) null);
+        m.setDetails((String) null);
         
         SoftSourceLocation sl = new SoftSourceLocation();
         sl.setFile((String) null); 
diff --git a/testing/src/org/aspectj/testing/xml/SoftMessage.java b/testing/src/org/aspectj/testing/xml/SoftMessage.java
index f11e55966..208b155b7 100644
--- a/testing/src/org/aspectj/testing/xml/SoftMessage.java
+++ b/testing/src/org/aspectj/testing/xml/SoftMessage.java
@@ -11,10 +11,10 @@
  *     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;
@@ -26,261 +26,294 @@ 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.
+ * This implementation is immutable if ISourceLocation is immutable,
+ * except for adding source locations.
  */
-public class SoftMessage implements IMessage { // XXX mutable dup of Message
-    public static String XMLNAME = "message";
-    public static final File NO_FILE = ISourceLocation.NO_FILE;
-    private String message;
-    private IMessage.Kind kind;
-    private Throwable thrown;
-    private ISourceLocation sourceLocation;
-    private String details;
+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 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 
-     * @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) {
-           out.printAttribute("message", value);
-       }
-       ISourceLocation sl = message.getSourceLocation();
-       if (null != sl) {
-           out.endAttributes();
-           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");
+	// 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("message", 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);            
+			}
         }
-        if (null == kind) {
-             throw new IllegalArgumentException("null kind");
-        }
-    }
-    
-    /** @return the kind of this message */
-    public IMessage.Kind getKind() {
-        return kind;
-    }
-
-    /** @return true if kind == IMessage.ERROR */
-    public boolean isError() {
-        return kind == IMessage.ERROR;
-    }
-    
-    /** @return true if kind == IMessage.WARNING */
-    public boolean isWarning() {
-        return kind == IMessage.WARNING;
-    }
-
-    /** @return true if kind == IMessage.DEBUG */
-    public boolean isDebug() {
-        return kind == IMessage.DEBUG;
-    }
-
-    /** 
-     * @return true if kind == IMessage.INFO  
-     */
-    public boolean isInfo() {
-        return kind == IMessage.INFO;
-    }
-    
-    /** @return true if  kind == IMessage.ABORT  */
-    public boolean isAbort() {
-        return kind == IMessage.ABORT;
-    }    
-    
-    /** 
-     * @return true if kind == IMessage.FAIL
-     */
-    public boolean isFailed() {
-        return kind == IMessage.FAIL;
-    }
-    
-    /** @return non-null String with simple message */
-    final public String getMessage() {
-        return message;
-    }
-    
-    /** @return Throwable associated with this message, or null if none */
-    final public Throwable getThrown() {
-        return thrown;
-    }
-
-    /** 
-     * This returns any ISourceLocation set or a mock-up
-     * if file and/or line were set.
-     * @return ISourceLocation associated with this message, 
-     * a mock-up if file or line is available, or null if none 
-     */
-    final public ISourceLocation 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(getKind().toString());
-        
-        String messageString = getMessage();
-        if (!LangUtil.isEmpty(messageString)) {
-            result.append(messageString);
-        }
+		out.endElement(elementName);
+	}
+
+	public SoftMessage() {
+	} // XXX programmatic only
+
+	/**
+	 * Create a (compiler) error or warning message
+	 * @param message the String used as the underlying message
+	 * @param sourceLocation the ISourceLocation, if any, associated with this message
+	 * @param isError if true, use IMessage.ERROR; else use IMessage.WARNING
+	 */
+	public SoftMessage(
+		String message,
+		ISourceLocation location,
+		boolean isError) {
+		this(
+			message,
+			(isError ? IMessage.ERROR : IMessage.WARNING),
+			null,
+			location);
+	}
+
+	/**
+	 * Create a message, handling null values for message and kind
+	 * if thrown is not null.
+	 * @param message the String used as the underlying message
+	 * @param kind the IMessage.Kind of message - not null
+	 * @param thrown the Throwable, if any, associated with this message
+	 * @param sourceLocation the ISourceLocation, if any, associated with this message
+	 * @throws IllegalArgumentException if message is null and
+	 * thrown is null or has a null message, or if kind is null
+	 * and thrown is null.
+	 */
+	public SoftMessage(
+		String message,
+		IMessage.Kind kind,
+		Throwable thrown,
+		ISourceLocation sourceLocation) {
+		this.message = message;
+		this.kind = kind;
+		this.thrown = thrown;
+		this.sourceLocation = sourceLocation;
+		if (null == message) {
+			if (null != thrown) {
+				message = thrown.getMessage();
+			}
+			if (null == message) {
+				throw new IllegalArgumentException("null message");
+			}
+		}
+		if (null == kind) {
+			throw new IllegalArgumentException("null kind");
+		}
+	}
+
+	/** @return the kind of this message */
+	public IMessage.Kind getKind() {
+		return kind;
+	}
+
+	/** @return true if kind == IMessage.ERROR */
+	public boolean isError() {
+		return kind == IMessage.ERROR;
+	}
+
+	/** @return true if kind == IMessage.WARNING */
+	public boolean isWarning() {
+		return kind == IMessage.WARNING;
+	}
+
+	/** @return true if kind == IMessage.DEBUG */
+	public boolean isDebug() {
+		return kind == IMessage.DEBUG;
+	}
+
+	/** 
+	 * @return true if kind == IMessage.INFO  
+	 */
+	public boolean isInfo() {
+		return kind == IMessage.INFO;
+	}
+
+	/** @return true if  kind == IMessage.ABORT  */
+	public boolean isAbort() {
+		return kind == IMessage.ABORT;
+	}
+
+	/** 
+	 * @return true if kind == IMessage.FAIL
+	 */
+	public boolean isFailed() {
+		return kind == IMessage.FAIL;
+	}
+
+	/** @return non-null String with simple message */
+	final public String getMessage() {
+		return message;
+	}
+
+	/** @return Throwable associated with this message, or null if none */
+	final public Throwable getThrown() {
+		return thrown;
+	}
+
+	/** 
+	 * This returns any ISourceLocation set or a mock-up
+	 * if file and/or line were set.
+	 * @return ISourceLocation associated with this message, 
+	 * a mock-up if file or line is available, or null if none 
+	 */
+	final public ISourceLocation 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();
+	}
 
-        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;
 	}
@@ -293,6 +326,11 @@ public class SoftMessage implements IMessage { // XXX mutable dup of Message
 	 * @see org.aspectj.bridge.IMessage#getExtraSourceLocations()
 	 */
 	public List getExtraSourceLocations() {
-		return Collections.EMPTY_LIST;
+		return extraSourceLocations;
+	}
+	public void addSourceLocation(ISourceLocation location) {
+		if (null != location) {
+			extraSourceLocations.add(location);
+		}
 	}
 }
diff --git a/testing/src/org/aspectj/testing/xml/SoftSourceLocation.java b/testing/src/org/aspectj/testing/xml/SoftSourceLocation.java
index 48d88db69..7df8436a0 100644
--- a/testing/src/org/aspectj/testing/xml/SoftSourceLocation.java
+++ b/testing/src/org/aspectj/testing/xml/SoftSourceLocation.java
@@ -1,6 +1,7 @@
 /* *******************************************************************
  * Copyright (c) 1999-2001 Xerox Corporation, 
- *               2002 Palo Alto Research Center, Incorporated (PARC).
+ *               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 Common Public License v1.0 
@@ -9,6 +10,7 @@
  *  
  * Contributors: 
  *     Xerox/PARC     initial implementation 
+ *     Wes Isberg     2004 updates
  * ******************************************************************/
 
 package org.aspectj.testing.xml;
@@ -20,17 +22,13 @@ import org.aspectj.bridge.ISourceLocation;
 import org.aspectj.util.LangUtil;
 
 /**
- * Immutable source location.
- * This guarantees that the source file is not null
- * and that the numeric values are positive and line <= endLine.
- * @see org.aspectj.lang.reflect.SourceLocation
- * @see org.aspectj.compiler.base.parser.SourceInfo
- * @see org.aspectj.tools.ide.SourceLine
- * @see org.aspectj.testing.harness.ErrorLine
+ * 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  { // XXX endLine?
-    public static final File NONE = new File("SoftSourceLocation.NONE");
-    public static final String XMLNAME = "source-location";
+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.
@@ -44,17 +42,16 @@ public class SoftSourceLocation implements ISourceLocation  { // XXX endLine?
        final String elementName = XMLNAME;
        out.startElement(elementName, false);
        out.printAttribute("line", "" + sl.getLine());
-       out.printAttribute("column", "" + sl.getColumn());
-       out.printAttribute("endLine", "" + sl.getEndLine());
+       // other attributes not supported
        File file = sl.getSourceFile();
        if (null != file) {
-           out.printAttribute("sourceFile", file.getPath());
+           out.printAttribute("file", file.getPath());
        }
        out.endElement(elementName);
     }
 
     private File sourceFile;
-    private int line;
+    private int line = -1; // required for no-line comparisons to work
     private int column;
     private int endLine;
     private String context;
@@ -83,13 +80,20 @@ public class SoftSourceLocation implements ISourceLocation  { // XXX endLine?
     public void setFile(String sourceFile) {
         this.sourceFile = new File(sourceFile);
     }
+    
+    public void setLine(String line) {
+        setLineAsString(line);
+    }
 
-    public void setLine(String  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);
@@ -115,7 +119,6 @@ public class SoftSourceLocation implements ISourceLocation  { // XXX endLine?
     public String toString() {
         return (null == context ? "" : context + LangUtil.EOL)
             + getSourceFile().getPath() 
-            + ":" + getLine() 
-            + ":" + getColumn();
+            + ":" + getLine() ;
     }
 }
-- 
2.39.5