// Copyright © 2011-2013, Esko Luontola <www.orfjackal.net> // This software is released under the Apache License 2.0. // The license text is at http://www.apache.org/licenses/LICENSE-2.0 package fi.jumi.core.util; import fi.jumi.actors.eventizers.EventToString; import java.lang.reflect.*; import java.util.*; import static org.hamcrest.MatcherAssert.assertThat; public class SpyListener<T> implements InvocationHandler { static final String ERROR_MARKER = " ^^^^^^^^^^^^^^"; private final Class<T> listenerType; private final List<Call> expectations = new ArrayList<>(); private final List<Call> actualCalls = new ArrayList<>(); private List<Call> current = expectations; public SpyListener(Class<T> listenerType) { this.listenerType = listenerType; } public T getListener() { Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{listenerType}, this); return listenerType.cast(proxy); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (args == null) { args = new Object[0]; } current.add(new Call(method.getName(), args)); return null; } public void replay() { if (current != expectations) { throw new IllegalStateException("replay() has already been called"); } current = actualCalls; } public void verify() { if (current != actualCalls) { throw new IllegalStateException("replay() was not called"); } StringBuilder message = new StringBuilder(); message.append("not all expectations were met\n"); message.append("Expected:\n"); for (int i = 0; i < expectations.size(); i++) { message.append(listItem(i, expectations)); if (!matchesAt(i)) { message.append(ERROR_MARKER + "\n"); } } message.append("but was:\n"); for (int i = 0; i < actualCalls.size(); i++) { message.append(listItem(i, actualCalls)); if (!matchesAt(i)) { message.append(ERROR_MARKER + "\n"); } } assertThat(message.toString(), actualCalls.equals(expectations)); } private boolean matchesAt(int i) { return i < actualCalls.size() && i < expectations.size() && expectations.get(i).equals(actualCalls.get(i)); } private static String listItem(int i, List<Call> list) { return " " + (i + 1) + ". " + list.get(i) + "\n"; } private static class Call { private final String methodName; private final Object[] args; public Call(String methodName, Object... args) { this.methodName = methodName; this.args = args; } @Override public String toString() { return EventToString.format("", methodName, args).substring(1); } @Override public boolean equals(Object obj) { Call that = (Call) obj; return this.methodName.equals(that.methodName) && argsMatch(that); } private boolean argsMatch(Call that) { if (this.args.length != that.args.length) { return false; } for (int i = 0; i < this.args.length; i++) { Object arg1 = this.args[i]; Object arg2 = that.args[i]; if (arg1 instanceof Throwable) { if (!sameTypeAndMessage((Throwable) arg1, (Throwable) arg2)) { return false; } } else if (!Objects.equals(arg1, arg2)) { return false; } } return true; } private boolean sameTypeAndMessage(Throwable t1, Throwable t2) { return Objects.equals(t1.getClass(), t2.getClass()) && Objects.equals(t1.getMessage(), t2.getMessage()); } } }