Diffs.java

/* *******************************************************************
 * Copyright (c) 1999-2001 Xerox Corporation,
 *               2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Xerox/PARC     initial implementation
 * ******************************************************************/

package org.aspectj.testing.util;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.util.TestDiffs.TestResult;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;

/**
 * Result struct for expected/actual diffs for Collection
 */
public class Diffs {

	/**
	 * Compare IMessage.Kind based on kind priority.
	 */
	public static final Comparator KIND_PRIORITY = new Comparator() {
		/**
		 * Compare IMessage.Kind based on kind priority.
		 * @throws NullPointerException if anything is null
		 */
		public int compare(Object lhs, Object rhs) {
			return ((IMessage.Kind) lhs).compareTo((IMessage.Kind) rhs);
		}
	};
	/**
	 * Sort ISourceLocation based on line, file path.
	 */
	public static final Comparator SORT_SOURCELOC = new Comparator() {
		/**
		 * Compare ISourceLocation based on line, file path.
		 * @throws NullPointerException if anything is null
		 */
		public int compare(Object lhs, Object rhs) {
			ISourceLocation l = (ISourceLocation) lhs;
			ISourceLocation r = (ISourceLocation) rhs;
			int result = getLine(l) - getLine(r);
			if (0 != result) {
				return result;
			}
			String lp = getSourceFile(l).getPath();
			String rp = getSourceFile(r).getPath();
			return lp.compareTo(rp);
		}
	};

	/**
	 * Compare IMessages based on kind and source location line (only).
	 */
	public static final Comparator<IMessage> MESSAGE_LINEKIND = new Comparator<IMessage>() {
		/**
		 * Compare IMessages based on kind and source location line (only).
		 * @throws NullPointerException if anything is null
		 */
		public int compare(IMessage lhs, IMessage rhs) {
			IMessage lm = lhs;
			IMessage rm = rhs;
            ISourceLocation ls = (lm == null ? null : lm.getSourceLocation());
            ISourceLocation rs = (rm == null ? null : rm.getSourceLocation());
            int left = (ls == null ? -1 : ls.getLine());
            int right = (rs == null ? -1 : rs.getLine());
			int result = left - right;
			if (0 == result) {
				result = lm.getKind().compareTo(rm.getKind());
			}
			return result;
		}
	};
	public static final Filter ACCEPT_ALL = new Filter() {
		public boolean accept(Object o) {
			return true;
		}
	};
	//     // XXX List -> Collection b/c comparator orders
	//    public static final Diffs NONE
	//        = new Diffs("NONE", Collections.EMPTY_LIST, Collections.EMPTY_LIST);

	public static Diffs makeDiffs(
		String label,
		List expected,
		List actual,
		Comparator comparator) {
		return makeDiffs(
			label,
			expected,
			actual,
			comparator,
			ACCEPT_ALL,
			ACCEPT_ALL);
	}

	public static Diffs makeDiffs(
		String label,
		IMessage[] expected,
		IMessage[] actual) {
		return makeDiffs(label, expected, actual, null, null);
	}

    private static int getLine(ISourceLocation loc) {
        int result = -1;
        if (null != loc) {
            result = loc.getLine();
        }
        return result;
    }
    private static int getLine(IMessage message) {
        int result = -1;
        if ((null != message)) {
            result = getLine(message.getSourceLocation());
        }
        return result;
    }

    private static File getSourceFile(ISourceLocation loc) {
        File result = ISourceLocation.NO_FILE;
        if (null != loc) {
            result = loc.getSourceFile();
        }
        return result;
    }

	public static Diffs makeDiffs(
		String label,
		IMessage[] expected,
		IMessage[] actual,
		IMessage.Kind[] ignoreExpectedKinds,
		IMessage.Kind[] ignoreActualKinds) {
		List<IMessage> exp = getExcept(expected, ignoreExpectedKinds);
		List<IMessage> act = getExcept(actual, ignoreActualKinds);

		List<IMessage> missing = new ArrayList<>();
		List<IMessage> 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;
			List expectedFound = new ArrayList();
			List 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.
	 * <pre>
	 *   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)
	 * <pre>
	 * @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 =
			expected.getExtraSourceLocations().toArray(
			new ISourceLocation[0]);
		ISourceLocation[] asl =
			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 (actual.contains(expected));
		}
	}

	private static List<IMessage> getExcept(
		IMessage[] source,
		IMessage.Kind[] skip) {
		List<IMessage> sink = new ArrayList<>();
		if (LangUtil.isEmpty(source)) {
			return sink;
		}

		if (LangUtil.isEmpty(skip)) {
			sink.addAll(Arrays.asList(source));
			sink.sort(MESSAGE_LINEKIND);
			return sink;
		}
		for (IMessage message : source) {
			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);
			}
		}
		sink.sort(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<TestResult> missing;

	/** immutable List */
	public final List<TestResult> 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
	 * <code>MessageUtil.renderMessage(IMessage)</code>.
	 * @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 (Object value : missing) {
				String s = MessageUtil.renderMessage((IMessage) value);
				MessageUtil.fail(handler, "missing " + label + ": " + s);
			}
			for (Object o : unexpected) {
				String s = MessageUtil.renderMessage((IMessage) o);
				MessageUtil.fail(handler, "unexpected " + label + ": " + s);
			}
		}
	}

	/** @return "{label}: (unexpected={#}, missing={#})" */
	public String toString() {
		return label
			+ "(unexpected="
			+ unexpected.size()
			+ ", missing="
			+ missing.size()
			+ ")";
	}
	public interface Filter {
		/** @return true to keep input in list of messages */
		boolean accept(Object input);
	}
}