/*******************************************************************************
* Copyright (c) 2008 BEA Systems, Inc.
* 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:
* wharley@bea.com - initial API and implementation
*
*******************************************************************************/
package org.eclipse.jdt.compiler.apt.tests.processors.base;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
/**
* Utility to compare two XML DOM trees that represent JSR269 (javax.lang.model) language models.
* This could be done with existing third-party XMLDiff tools and a sufficiently articulate DTD, so
* if it needs to be substantially enhanced at some point in the future, maintainers should consider
* using that approach instead.
*
* This is not a generic XML comparison tool; it has specific expectations about the structure of a
* JSR269 model, for example, that declarations may contain annotations but not vice versa.
*
* Note that in this body of code, we use the term "Decl" or "Declaration" to refer to the the
* entities represented by javax.lang.model.element.Element, to avoid confusion with XML elements,
* i.e. org.w3c.dom.Element.
*
* @since 3.4
*/
public class XMLComparer implements IXMLNames {
/**
* A structure to collect and organize all the contents of an <elements> node, that is,
* all the things that the {@link javax.lang.model.element.Element#getEnclosedElements()} method
* would return. The key is the simple name of the entity. The reason this is needed is because
* simple names can be repeated, e.g., a class may contain both a method and a nested class with
* the same name.
*
* The structure also has a holder for an <annotations> node, as a convenience, because
* when searching the XML DOM we discover this node at the same time as the element
* declarations.
*
* @since 3.4
*/
private class DeclarationContents {
Element annotations = null;
Element superclass = null;
Element interfaces = null;
final TreeMap<String, Element> typeDecls = new TreeMap<String, Element>();
final TreeMap<String, Element> executableDecls = new TreeMap<String, Element>();
final TreeMap<String, Element> variableDecls = new TreeMap<String, Element>();
// TODO: PACKAGE, TYPE_PARAMETER, OTHER
}
/**
* Compare two JSR269 language models, using the approximate criteria of the JSR269 spec. Ignore
* differences in order of sibling elements. If the two do not match, optionally send detailed
* information about the mismatch to an output stream.
*
* @param actual
* the observed language model
* @param expected
* the reference language model
* @param out
* a stream to which detailed information on mismatches will be output. Can be null
* if no detailed information is desired.
* @param summary
* a StringBuilder to which will be appended a brief summary of the problem if a
* problem was encountered. Can be null if no summary is desired.
* @param ignoreJavacBugs
* true if mismatches corresponding to known javac bugs should be ignored.
* @return true if the models match sufficiently to satisfy the spec.
*/
public static boolean compare(Document actual, Document expected,
OutputStream out, StringBuilder summary, boolean ignoreJavacBugs) {
XMLComparer comparer = new XMLComparer(actual, expected, out, summary, ignoreJavacBugs);
return comparer._compare();
}
private final Document _actual;
private final Document _expected;
/**
* If true, don't complain about mismatches corresponding to known javac bugs,
* even if they represent a violation of the spec. This is useful when running
* tests against the reference implementation.
*/
private final boolean _ignoreJavacBugs;
private final PrintStream _out;
private final StringBuilder _summary;
/**
* Clients should not construct instances of this object.
*/
private XMLComparer(Document actual, Document expected, OutputStream out, StringBuilder summary, boolean ignoreJavacBugs) {
_actual = actual;
_expected = expected;
_ignoreJavacBugs = ignoreJavacBugs;
OutputStream os;
if (out != null) {
os = out;
} else {
os = new OutputStream() {
public void write(int b) throws IOException {
// do nothing
}
};
}
_out = new PrintStream(os, true);
_summary = summary;
}
/**
* Test this class by creating a known XML language model and using
* this class to compare it to a known reference model. The models
* should match.
* @return true if the models matched, i.e., if the test passed
* @throws Exception
*/
public static boolean test() throws Exception {
final String XML_FRAMEWORK_TEST_MODEL =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
"<model>\n" +
" <type-element kind=\"CLASS\" qname=\"pa.A\" sname=\"A\">\n" +
" <superclass>\n" +
" <type-mirror kind=\"DECLARED\" to-string=\"java.lang.Object\"/>\n" +
" </superclass>\n" +
" <variable-element kind=\"FIELD\" sname=\"f\" type=\"java.lang.String\">\n" +
" <annotations>\n" +
" <annotation sname=\"Anno1\">\n" +
" <annotation-values>\n" +
" <annotation-value member=\"value\" type=\"java.lang.String\" value=\"spud\"/>\n" +
" </annotation-values>\n" +
" </annotation>\n" +
" </annotations>\n" +
" </variable-element>\n" +
" </type-element>\n" +
"</model>\n";
// create "actual" model
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document actualModel = factory.newDocumentBuilder().newDocument();
Element modelNode = actualModel.createElement(MODEL_TAG);
// primary type
Element typeNode = actualModel.createElement(TYPE_ELEMENT_TAG);
typeNode.setAttribute(KIND_TAG, "CLASS");
typeNode.setAttribute(SNAME_TAG, "A");
typeNode.setAttribute(QNAME_TAG, "pa.A");
// superclass
Element scNode = actualModel.createElement(SUPERCLASS_TAG);
Element tmNode = actualModel.createElement(TYPE_MIRROR_TAG);
tmNode.setAttribute(KIND_TAG, "DECLARED");
tmNode.setAttribute(TO_STRING_TAG, "java.lang.Object");
scNode.appendChild(tmNode);
typeNode.appendChild(scNode);
// field
Element variableNode = actualModel.createElement(VARIABLE_ELEMENT_TAG);
variableNode.setAttribute(KIND_TAG, "FIELD");
variableNode.setAttribute(SNAME_TAG, "f");
variableNode.setAttribute(TYPE_TAG, "java.lang.String");
// annotation on field
Element annotationsNode = actualModel.createElement(ANNOTATIONS_TAG);
Element annoNode = actualModel.createElement(ANNOTATION_TAG);
annoNode.setAttribute(SNAME_TAG, "Anno1");
Element valuesNode = actualModel.createElement(ANNOTATION_VALUES_TAG);
Element valueNode = actualModel.createElement(ANNOTATION_VALUE_TAG);
valueNode.setAttribute(MEMBER_TAG, "value");
valueNode.setAttribute(TYPE_TAG, "java.lang.String");
valueNode.setAttribute(VALUE_TAG, "spud");
valuesNode.appendChild(valueNode);
annoNode.appendChild(valuesNode);
annotationsNode.appendChild(annoNode);
variableNode.appendChild(annotationsNode);
typeNode.appendChild(variableNode);
modelNode.appendChild(typeNode);
actualModel.appendChild(modelNode);
// load reference model
InputSource source = new InputSource(new StringReader(XML_FRAMEWORK_TEST_MODEL));
Document expectedModel = factory.newDocumentBuilder().parse(source);
// compare actual and reference
ByteArrayOutputStream out = new ByteArrayOutputStream();
StringBuilder summary = new StringBuilder();
summary.append("testXMLFramework failed; see console for details. ");
boolean success = compare(actualModel, expectedModel, out, summary, false /* ignoreJavacBugs */);
if (!success) {
System.out.println("testXMLFramework failed. Detailed output follows:");
System.out.print(out.toString());
System.out.println("=============== end output ===============");
}
return success;
}
/**
* Non-static internal comparison routine called from
* {@link #compare(Document, Document, OutputStream)}
*
* @return true if models are equivalent
*/
private boolean _compare() {
// navigate to the outermost <model> nodes of each document
Element actualModel = findRootNode(_actual);
Element expectedModel = findRootNode(_expected);
if (actualModel == null) {
if (expectedModel == null) {
return true;
}
printProblem("Actual model contained no <elements> node.");
printDifferences();
return false;
}
if (expectedModel == null) {
printProblem("Actual model contained unexpected elements.");
printDifferences();
return false;
}
return compareDeclarations(actualModel, expectedModel);
}
/**
* Collect the contents of an <annotations> node into a map. If there are declarations of
* the same name, report an error; if there are unexpected contents (e.g., declarations, which
* should not be contained within an annotations node), report an error.
*
* TODO: revisit this - we need to model duplications, in order to handle incorrect code.
*
* @param annotsNode
* must not be null
* @param map
* a map from annotation type name to the XML node representing the annotation
* instance
* @return true if no errors were reported
*/
private boolean collectAnnotations(Element annotsNode, Map<String, Element> map) {
for (Node n = annotsNode.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element e = (Element)n;
String nodeName = e.getNodeName();
// get 'sname'
String sname = e.getAttribute(SNAME_TAG);
if (sname == null) {
printProblem("A child of an <annotations> node was missing the \"sname\" attribute");
printDifferences();
return false;
}
// categorize
Element old = null;
if (ANNOTATION_TAG.equals(nodeName)) {
old = map.put(sname, e);
} else {
printProblem("An <annotations> node unexpectedly contained something other than <annotation>: "
+ nodeName);
printDifferences();
return false;
}
if (old != null) {
printProblem("Two sibling annotation mirrors had the same sname: " + sname);
printDifferences();
return false;
}
}
return true;
}
/**
* Collect the contents of a declaration, including child declarations and annotations, into a
* collection of maps. If there are declarations of the same type and simple name, report an
* error; if there are unexpected contents), report an error.
* TODO: revisit this - we need to model duplications, in order to handle incorrect code.
*
* @param elementNode
* must not be null
* @param contents
* must not be null
* @return true if no errors were reported
*/
private boolean collectDeclarationContents(Element declarationNode, DeclarationContents contents) {
for (Node n = declarationNode.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element e = (Element)n;
String nodeName = e.getNodeName();
if (ANNOTATIONS_TAG.equals(nodeName)) {
if (contents.annotations != null) {
printProblem("XML syntax error: a declaration contained more than one <annotations> node");
printDifferences();
return false;
}
contents.annotations = e;
} else if (SUPERCLASS_TAG.equals(nodeName)) {
if (contents.superclass != null) {
printProblem("XML syntax error: a declaration contained more than one <superclass> node");
printDifferences();
return false;
}
contents.superclass = e;
} else if (INTERFACES_TAG.equals(nodeName)) {
if (contents.interfaces != null) {
printProblem("XML syntax error: a declaration contained more than one <interfaces> node");
printDifferences();
return false;
}
contents.interfaces = e;
} else {
// get 'sname'
String sname = e.getAttribute(SNAME_TAG);
if (sname == null) {
printProblem("A child of an <elements> node was missing the \"sname\" attribute");
printDifferences();
return false;
}
// categorize
Element old = null;
if (EXECUTABLE_ELEMENT_TAG.equals(nodeName)) {
old = contents.executableDecls.put(sname, e);
} else if (TYPE_ELEMENT_TAG.equals(nodeName)) {
old = contents.typeDecls.put(sname, e);
} else if (VARIABLE_ELEMENT_TAG.equals(nodeName)) {
old = contents.variableDecls.put(sname, e);
} else {
printProblem("A declaration contained an unexpected child node: " + nodeName);
printDifferences();
return false;
}
if (old != null) {
printProblem("Two elements of the same kind had the same sname: " + sname);
printDifferences();
return false;
}
}
}
return true;
}
/**
* Collect the <type-mirror> children of a parent node into a map,
* keyed and sorted by the canonicalized type name.
* For now, we use the toString() output as the canonical name, even though
* that is unspecified and implementation-dependent.
* Reject duplicated types.
* TODO: revisit this - we need to model duplications, in order to handle incorrect code.
* @param parent the parent node
* @param typesMap the map, presumed to be empty on entry
* @return true if no errors were reported
*/
private boolean collectTypes(Node parent, Map<String, Element> typesMap) {
for (Node n = parent.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeType() == Node.ELEMENT_NODE & TYPE_MIRROR_TAG.equals(n.getNodeName())) {
Element typeMirror = (Element)n;
String toStringAttr = typeMirror.getAttribute(TO_STRING_TAG);
if (null == toStringAttr || toStringAttr.length() < 1) {
printProblem("<type-mirror> node was missing its \"to-string\" attribute");
printDifferences();
return false;
}
Element old = typesMap.put(toStringAttr, typeMirror);
if (null != old) {
printProblem("Two <type-mirror> nodes had the same \"to-string\" attribute: " + toStringAttr);
printDifferences();
return false;
}
}
}
return true;
}
/**
* Compare an actual annotation mirror to the expected reference. It is assumed that the
* annotation's sname has already been compared. Attributes that exist on the actual annotation
* but not on the expected annotation are not considered to be a mismatch. Note that the
* language model representation in XML does not include default values, since these are
* attributes of the annotation type rather than the annotation instance.
*
* @param actualAnnot
* must be non-null
* @param expectedAnnot
* must be non-null
* @return true if the elements match
*/
private boolean compareAnnotationNodes(Element actualAnnot, Element expectedAnnot) {
// Compare attributes of the annotation instances
// Intentionally ignore the presence of additional actual attributes not present in the
// expected model
NamedNodeMap expectedAttrs = expectedAnnot.getAttributes();
NamedNodeMap actualAttrs = actualAnnot.getAttributes();
int numAttrs = expectedAttrs.getLength();
String sname = null;
for (int i = 0; i < numAttrs; ++i) {
Node expectedAttr = expectedAttrs.item(i);
String attrName = expectedAttr.getNodeName();
Node actualAttr = actualAttrs.getNamedItem(attrName);
if (actualAttr == null) {
printProblem("Actual annotation mirror was missing expected attribute: " + attrName);
printDifferences();
return false;
}
String expectedValue = expectedAttr.getNodeValue();
String actualValue = actualAttr.getNodeValue();
if (!expectedValue.equals(actualValue)) {
printProblem("Actual attribute value was different than expected: attribute "
+ attrName + ", expected " + expectedValue + ", actual " + actualValue);
printDifferences();
return false;
}
if (SNAME_TAG.equals(attrName)) {
sname = actualValue;
}
}
// Examine member-value pairs
Element actualValues = findNamedChildElement(actualAnnot, ANNOTATION_VALUES_TAG);
Element expectedValues = findNamedChildElement(expectedAnnot, ANNOTATION_VALUES_TAG);
if (actualValues != null && expectedValues != null) {
if (!compareAnnotationValuesNodes(actualValues, expectedValues)) {
return false;
}
}
else if (actualValues != null) {
// expectedValues == null
printProblem("Found unexpected <annotation-values> in annotation: " + sname);
printDifferences();
return false;
}
else if (expectedValues != null) {
// actualValues == null
printProblem("Missing expected <annotation-values> in annotation: " + sname);
printDifferences();
return false;
}
// both null is okay
return true;
}
/**
* Compare the contents of two <annotations> nodes.
*
* @param actualAnnots
* may be empty, but must not be null
* @param expectedAnnots
* may be empty, but must not be null
* @return true if the contents are equivalent.
*/
private boolean compareAnnotationsNodes(Element actualAnnots, Element expectedAnnots) {
// Group declarations alphabetically so they can be compared
Map<String, Element> actual = new TreeMap<String, Element>();
Map<String, Element> expected = new TreeMap<String, Element>();
if (!collectAnnotations(actualAnnots, actual))
return false;
if (!collectAnnotations(expectedAnnots, expected))
return false;
// Compare the collections at this level
if (!actual.keySet().equals(expected.keySet())) {
printProblem("Contents of <annotations> nodes did not match");
printDifferences();
return false;
}
// Compare individual annotations in more detail
for (Map.Entry<String, Element> expectedEntry : expected.entrySet()) {
String sname = expectedEntry.getKey();
Element actualElement = actual.get(sname);
if (!compareAnnotationNodes(actualElement, expectedEntry.getValue())) {
return false;
}
}
return true;
}
/**
* Compare the contents of two <annotation-values> nodes; that is, compare
* actual and expected lists of annotation member/value pairs. These lists do
* not typically include annotation value defaults, since those are an attribute
* of an annotation type rather than an annotation instance. Ordering of the list
* is important: the same pairs, in a different order, is considered a mismatch.
* @param actual must be non-null
* @param expected must be non-null
* @return true if the sets are equivalent
*/
private boolean compareAnnotationValuesNodes(Element actual, Element expected) {
Node nActual = actual.getFirstChild();
for (Node nExpected = expected.getFirstChild(); nExpected != null; nExpected = nExpected.getNextSibling()) {
if (nExpected.getNodeType() == Node.ELEMENT_NODE && ANNOTATION_VALUE_TAG.equals(nExpected.getNodeName())) {
while (nActual != null &&
(nActual.getNodeType() != Node.ELEMENT_NODE ||
!ANNOTATION_VALUE_TAG.equals(nActual.getNodeName()))) {
nActual = nActual.getNextSibling();
}
if (nActual == null) {
printProblem("Annotation member-value pairs were different: expected more pairs than were found");
printDifferences();
return false;
}
// Now we've got two annotation-value elements; compare their attributes.
// We will ignore "extra" (unexpected) attributes in the actual model.
if (!compareAttributes(nActual, nExpected)) {
return false;
}
}
}
return true;
}
/**
* Compare the attributes of two nodes. Ignore attributes that are found on
* the actual, but not expected, node.
* @param actual must not be null
* @param expected must not be null
* @return true if the attribute lists are equivalent
*/
private boolean compareAttributes(Node actual, Node expected) {
NamedNodeMap expectedAttrs = expected.getAttributes();
NamedNodeMap actualAttrs = actual.getAttributes();
for (int i = 0; i < expectedAttrs.getLength(); ++i) {
Node expectedAttr = expectedAttrs.item(i);
String attrName = expectedAttr.getNodeName();
if (OPTIONAL_TAG.equals(attrName)) {
// "optional" is an instruction to the comparer, not a model attribute
continue;
}
Node actualAttr = actualAttrs.getNamedItem(attrName);
if (actualAttr == null) {
printProblem("Actual element was missing expected attribute: " + attrName);
printDifferences();
return false;
}
String expectedValue = expectedAttr.getNodeValue();
String actualValue = actualAttr.getNodeValue();
if (!expectedValue.equals(actualValue)) {
printProblem("Actual attribute value was different than expected: attribute "
+ attrName + ", expected " + expectedValue + ", actual " + actualValue);
printDifferences();
return false;
}
}
return true;
}
/**
* Compare the sets of element declarations nested within an actual and an expected element.
* Note that the DeclarationContents object also may contain an <annotations> node,
* but that must be compared separately.
* If an expected element has the "optional" attribute, it is allowed to be missing from
* the actual contents. It is always a mismatch if the actual contents include an element
* that is not in the expected contents, though.
*
* @param actual
* must not be null
* @param expected
* must not be null
* @return true if the contents are equivalent.
*/
private boolean compareDeclarationContents(DeclarationContents actual, DeclarationContents expected) {
// Compare each collection at this level
if (!optionalMatch(actual.typeDecls, expected.typeDecls)) {
printProblem("Contents of <elements> nodes did not match: different sets of type-elements");
printDifferences();
return false;
}
if (!optionalMatch(actual.executableDecls, expected.executableDecls)) {
printProblem("Contents of <elements> nodes did not match: different sets of executable-elements");
printDifferences();
return false;
}
if (!optionalMatch(actual.variableDecls, expected.variableDecls)) {
printProblem("Contents of <elements> nodes did not match: different sets of variable-elements");
printDifferences();
return false;
}
// Recurse by comparing individual elements
for (Map.Entry<String, Element> expectedEntry : expected.typeDecls.entrySet()) {
String sname = expectedEntry.getKey();
Element actualElement = actual.typeDecls.get(sname);
if (actualElement != null && !compareDeclarations(actualElement, expectedEntry.getValue())) {
return false;
}
}
for (Map.Entry<String, Element> expectedEntry : expected.executableDecls.entrySet()) {
String sname = expectedEntry.getKey();
Element actualElement = actual.executableDecls.get(sname);
if (actualElement != null && !compareDeclarations(actualElement, expectedEntry.getValue())) {
return false;
}
}
for (Map.Entry<String, Element> expectedEntry : expected.variableDecls.entrySet()) {
String sname = expectedEntry.getKey();
Element actualElement = actual.variableDecls.get(sname);
if (actualElement != null && !compareDeclarations(actualElement, expectedEntry.getValue())) {
return false;
}
}
return true;
}
/**
* Compare an actual declaration to the expected reference. It is assumed that the element name
* (e.g., type-element, variable-element) and sname (e.g., "Foo") have already been compared.
* Attributes that exist on the actual declaration element but not on the expected declaration
* element are not considered to be a mismatch.
*
* @param actualDecl
* must be non-null
* @param expectedDecl
* must be non-null
* @return true if the declarations are equivalent
*/
private boolean compareDeclarations(Element actualDecl, Element expectedDecl) {
// compare the element kinds and any other relevant attributes
// Intentionally ignore the presence of additional actual attributes not present in the
// expected model
if (!compareAttributes(actualDecl, expectedDecl)) {
return false;
}
// Find nested element and <annotations> nodes
DeclarationContents actualContents = new DeclarationContents();
if (!collectDeclarationContents(actualDecl, actualContents)) {
return false;
}
DeclarationContents expectedContents = new DeclarationContents();
if (!collectDeclarationContents(expectedDecl, expectedContents)) {
return false;
}
// compare annotations on the element
if (actualContents.annotations != null && expectedContents.annotations != null) {
if (!compareAnnotationsNodes(actualContents.annotations, expectedContents.annotations)) {
return false;
}
} else if (actualContents.annotations != null) {
// expectedAnnots == null
printProblem("Unexpected annotations within element: "
+ expectedDecl.getAttribute(SNAME_TAG));
printDifferences();
return false;
} else if (expectedContents.annotations != null) {
// actualAnnots == null
printProblem("Missing expected annotations within element: "
+ actualDecl.getAttribute(SNAME_TAG));
printDifferences();
return false;
}
// both null at the same time is okay, not a mismatch
// compare superclasses. Ignore if reference does not specify a superclass.
if (expectedContents.superclass != null) {
if (actualContents.superclass == null) {
printProblem("No superclass found for element: " + actualDecl.getAttribute(SNAME_TAG));
printDifferences();
return false;
}
if (!compareSuperclassNodes(actualContents.superclass, expectedContents.superclass)) {
return false;
}
}
// compare interface lists. Ignore if reference does not specify interfaces.
// TODO: javac fails to provide unresolved interfaces. Here, we ignore interfaces altogether
// if we're ignoring javac bugs, which means we also ignore the non-error cases.
if (expectedContents.interfaces != null && !_ignoreJavacBugs) {
if (actualContents.interfaces == null) {
printProblem("No interfaces list found for element: " + actualDecl.getAttribute(SNAME_TAG));
printDifferences();
return false;
}
if (!compareInterfacesNodes(actualContents.interfaces, expectedContents.interfaces)) {
return false;
}
}
// compare the child elements
if (!compareDeclarationContents(actualContents, expectedContents)) {
return false;
}
return true;
}
/**
* Compare two interface lists, i.e., <interfaces> nodes.
* Each is expected to contain zero or more <type-mirror> nodes.
* The spec for {@link javax.lang.model.element.TypeElement#getInterfaces()}
* does not say anything about the order of the items returned, so here we
* load them into a Map<String, Element> keyed by the type's toString()
* output. Note that toString() on a TypeMirror is not very well
* specified either, so this is not guaranteed to produce good results.
* @param actual the observed <interfaces> node, must be non-null.
* @param expected the reference <interfaces> node, must be non-null
* @return true if the nodes are equivalent.
*/
private boolean compareInterfacesNodes(Element actual, Element expected) {
Map<String, Element> expectedTypes = new TreeMap<String, Element>();
Map<String, Element> actualTypes = new TreeMap<String, Element>();
if (!collectTypes(expected, expectedTypes)) {
return false;
}
if (!collectTypes(actual, actualTypes)) {
return false;
}
if (expectedTypes.size() != actualTypes.size()) {
if (_ignoreJavacBugs) {
// javac has a known bug where it does not correctly model
// unresolved interface types. Ideally we could still verify
// the resolved ones but that seems like more work than it's worth.
return true;
}
printProblem("Actual and expected interface lists have different sizes: expected = " +
expectedTypes.size() + ", actual = " + actualTypes.size());
printDifferences();
return false;
}
Iterator<Entry<String, Element>> expectedIter = expectedTypes.entrySet().iterator();
Iterator<Entry<String, Element>> actualIter = actualTypes.entrySet().iterator();
// if we got this far, the two maps are the same size
while (expectedIter.hasNext()) {
Entry<String, Element> expectedEntry = expectedIter.next();
Entry<String, Element> actualEntry = actualIter.next();
if (!compareTypeMirrors(actualEntry.getValue(), expectedEntry.getValue())) {
return false;
}
}
return true;
}
/**
* Compare two <superclass> nodes. Each is expected to contain
* exactly one <type-mirror> node.
*
* @param actual the observed <superclass> node; must not be null
* @param expected the reference <superclass> node; must not be null
* @return true if the superclass types are equivalent
*/
private boolean compareSuperclassNodes(Element actual, Element expected) {
Element expectedType = findNamedChildElement(expected, TYPE_MIRROR_TAG);
if (expectedType == null) {
// Syntax error in the reference model, i.e., problem in test code
printProblem("Bug in reference model: a <superclass> node was missing its <type-mirror> element");
printDifferences();
return false;
}
Element actualType = findNamedChildElement(actual, TYPE_MIRROR_TAG);
if (actualType == null) {
// This probably indicates a problem in the XMLConverter class
printProblem("Bug in test code: a <superclass> node was missing its <type-mirror> element in the XML model of the observed language model");
printDifferences();
return false;
}
return compareTypeMirrors(actualType, expectedType);
}
private boolean compareTypeMirrors(Element actual, Element expected) {
String expectedKind = expected.getAttribute(KIND_TAG);
if (expectedKind != null && expectedKind.length() > 0) {
String actualKind = actual.getAttribute(KIND_TAG);
if (!expectedKind.equals(actualKind)) {
printProblem("Superclasses had different kind: expected " + expectedKind + " but found " + actualKind);
printDifferences();
return false;
}
}
if (!TYPEKIND_ERROR.equals(expectedKind)) {
String expectedToString = expected.getAttribute(TO_STRING_TAG);
if (expectedToString != null && expectedToString.length() > 0) {
String actualToString = actual.getAttribute(TO_STRING_TAG);
if (!expectedToString.equals(actualToString)) {
printProblem("Superclasses had different toString() output: expected " + expectedToString + " but found " + actualToString);
printDifferences();
return false;
}
}
}
return true;
}
/**
* Given some non-null parent, find the first child element with a particular name
* @return the child, or null if one was not found
*/
private Element findNamedChildElement(Node parent, String name) {
for (Node n = parent.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeType() == Node.ELEMENT_NODE && name.equals(n.getNodeName())) {
return (Element)n;
}
}
return null;
}
/**
* Locate the outer <model> node. This node should always exist unless the model is
* completely empty.
*
* @return the root model node, or null if one could not be found.
*/
private Element findRootNode(Document doc) {
return findNamedChildElement(doc, MODEL_TAG);
}
/**
* Compare actual and expected. Ignore the presence of any elements in
* 'expected' that are absent from 'actual' iff the elements are tagged
* with the "optional" attribute.
* @return true if the collections match.
*/
private boolean optionalMatch(Map<String, Element> actual, Map<String, Element> expected) {
// Does actual contain anything that is not in expected?
Set<String> extraActuals = new HashSet<String>(actual.keySet());
extraActuals.removeAll(expected.keySet());
if (!extraActuals.isEmpty()) {
return false;
}
// Does expected contain anything that is not in actual, that is not optional?
Set<String> extraExpecteds = new HashSet<String>(expected.keySet());
extraExpecteds.removeAll(actual.keySet());
Iterator<String> iter = extraExpecteds.iterator();
while (iter.hasNext()) {
Element e = expected.get(iter.next());
boolean optional = Boolean.parseBoolean(e.getAttribute(OPTIONAL_TAG));
if (optional) {
iter.remove();
}
}
return extraExpecteds.isEmpty();
}
/**
* Print the actual and expected documents in string form
*
* TODO: a cursor to show what was being compared when the difference was detected.
*/
private void printDifferences() {
_out.println("Actual was:\n--------");
_out.println(XMLConverter.xmlToString(_actual));
_out.println("--------\nAnd expected was:");
_out.println(XMLConverter.xmlToString(_expected));
}
/**
* Report a specific problem.
*/
private void printProblem(String msg) {
if (_summary != null) {
_summary.append(msg);
}
_out.println(msg);
}
}