/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.polimi.zarathustra; import java.io.File; import java.io.IOException; import java.util.List; import org.custommonkey.xmlunit.DetailedDiff; import org.custommonkey.xmlunit.Diff; import org.custommonkey.xmlunit.Difference; import org.custommonkey.xmlunit.XMLUnit; import org.polimi.zarathustra.rules.RulesJsonSerializer; import org.w3c.dom.Document; import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.io.Files; /** * An helper class to compare and manipulate Differences in Zarathustra. */ public final class DifferenceHelper { /** * Only returns the differences in {@code differences} that are not present in {@code whitelist}. * Uses the XPath of the Difference to determine equality. */ private static List<Difference> differencesDiff(List<Difference> whitelist, List<Difference> differences) { List<Difference> uniqueDifferences = Lists.newArrayList(); for (Difference difference : differences) { if ((difference.getId() == 2) || (difference.getId() == 3) || (difference.getId() == 14) || (difference.getId() == 22)) { if (!isFalseDifference(difference, whitelist)) { uniqueDifferences.add(difference); } } } return uniqueDifferences; } /** * Returns true if the two documents are functionally identical, i.e. they don't differ in * anything but comments, ordering of attributes, comments and so on. */ public static boolean domIsEqual(Document first, Document second) { setupXmlUnit(); Diff diff = XMLUnit.compareXML(first, second); return diff.identical(); } /** * Compares two Documents stored into files. * * @param firstDocument First file containing a Document. * @param secondDocument Second file containing a Document. * @return true if the two serialized representations are equal, false otherwise. * @throws IOException If an error occurs opening the files or loading the class. */ public static boolean domIsEqual(File firstDocument, File secondDocument) throws IOException { Document first = DOMHelper.deserializeDocument(firstDocument); Document second = DOMHelper.deserializeDocument(secondDocument); return domIsEqual(first, second); } /** * Saves a text representation of the differences in a file. */ public static void dumpDifferences(List<Difference> differences, String filePath) throws IOException { StringBuilder sb = new StringBuilder(); RulesJsonSerializer serializer = new RulesJsonSerializer(filePath + ".json"); for (Difference difference : differences) { sb.append( "--- Difference " + difference.getId() + " at " + difference.getTestNodeDetail().getXpathLocation() + " ---\n" + difference.toString()).append("\n\n"); } File dumpFile = new File(filePath); Files.write(sb.toString(), dumpFile, Charsets.UTF_8); serializer.printDifferencesToFile(differences); } @SuppressWarnings("unchecked") private static List<Difference> getDifferences(Document base, List<Document> targets) { base.normalizeDocument(); List<Difference> differences = Lists.newArrayList(); for (Document target : targets) { target.normalizeDocument(); DetailedDiff myDiff = new DetailedDiff(XMLUnit.compareXML(base, target)); differences.addAll(myDiff.getAllDifferences()); } return differences; } /** * Performs comparison between two or more dumps of the same page. * * @param base the base document to compare * @param verifications a list of documents to use as a baseline. All differences between these * documents and the base document will be ignored with the target. * @param target a target document to compare with the base */ public static List<Difference> getDifferences(Document base, List<Document> verifications, Document target) throws IOException { setupXmlUnit(); // Gets the differences between the various "base" reference docs. List<Difference> whitelist = getDifferences(base, verifications); // Generates the differences between the base and the target. List<Difference> targetDifferences = getDifferences(base, ImmutableList.of(target)); // Filters out all the differences that were observed in the base reference. return differencesDiff(whitelist, targetDifferences); } public static List<Difference> getDifferences(File firstDocument, File secondDocument) throws IOException { return getDifferences(firstDocument, ImmutableList.<File>of(), secondDocument); } /** * Performs comparison between three or more dumps of the same page. Two are the base dump and the * actual target, while the others are more baseline dumps. Only differences that are not already * present in the base dumps will be reported. */ public static List<Difference> getDifferences(File baseDocument, List<File> verificationDocuments, File targetDocument) throws IOException { Document base = DOMHelper.deserializeDocument(baseDocument); List<Document> verifications = Lists.newArrayList(); for (File verificationDocument : verificationDocuments) { verifications.add(DOMHelper.deserializeDocument(verificationDocument)); } Document target = DOMHelper.deserializeDocument(targetDocument); return getDifferences(base, verifications, target); } /** * Compares a Difference with a list of Differences returning true if the Difference is in the * list. The comparison is based on both test and control nodes' xpaths. */ private static boolean isFalseDifference(Difference difference, List<Difference> whitelist) { String diffXpathOnControlNode = Strings.nullToEmpty(difference.getControlNodeDetail().getXpathLocation()); String diffXpathOnTestNode = Strings.nullToEmpty(difference.getTestNodeDetail().getXpathLocation()); String falsePositiveXPathOnControlNode; String falsePositiveXPathOnTestNode; if (matchesFalsePositiveHeuristics(difference)) { return true; } for (Difference whitelistedDifference : whitelist) { falsePositiveXPathOnControlNode = Strings.nullToEmpty(whitelistedDifference.getControlNodeDetail().getXpathLocation()); falsePositiveXPathOnTestNode = Strings.nullToEmpty(whitelistedDifference.getTestNodeDetail().getXpathLocation()); if (diffXpathOnTestNode.equalsIgnoreCase(falsePositiveXPathOnTestNode) && diffXpathOnControlNode.equalsIgnoreCase(falsePositiveXPathOnControlNode)) { return true; } } return false; } /** * Returns a boolean value resulting from the checks of all heuristics. If at least one heuristic * holds, the value true is returned and the difference is handles as a false one. */ private static boolean matchesFalsePositiveHeuristics(Difference difference) { return Heuristics.onBlacklistedElement(difference) || Heuristics.editedElementOnDifferentXpath(difference) || Heuristics.differentTextNotInScript(difference) || Heuristics.differentValueAttributeOnInput(difference); } /** Reads the dump of differences in a file, returning the JSON encoded differences. */ public static List<String> readDifferenceDump(File savedDifferences) throws IOException { // TODO(claudio): decode the differences. return Files.readLines(savedDifferences, Charsets.UTF_8); } private static final void setupXmlUnit() { XMLUnit.setIgnoreComments(true); XMLUnit.setIgnoreDiffBetweenTextAndCDATA(true); XMLUnit.setIgnoreWhitespace(true); XMLUnit.setIgnoreAttributeOrder(true); XMLUnit.setCompareUnmatched(true); } }