// Copyright 2017 The Nomulus Authors. 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 google.registry.util; import com.google.common.reflect.Reflection; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; import javax.annotation.Nullable; /** * Abstract InvocationHandler comparing two implementations of some interface. * *

Given an interface, and two instances of that interface (the "original" instance we know * works, and a "second" instance we wish to test), creates an InvocationHandler that acts like an * exact proxy to the "original" instance. * *

In addition, it will log any differences in return values or thrown exception between the * "original" and "second" instances. * *

This can be used to create an exact proxy to the original instance that can be placed in any * code, while live testing the second instance. */ public abstract class ComparingInvocationHandler implements InvocationHandler { private final T actualImplementation; private final T secondImplementation; private final Class interfaceClass; /** * Creates a new InvocationHandler for the given interface. * * @param interfaceClass the interface we want to create. * @param actualImplementation the resulting proxy will be an exact proxy to this object * @param secondImplementation Only used to log difference compared to actualImplementation. * Otherwise has no effect on the resulting proxy's behavior. */ public ComparingInvocationHandler( Class interfaceClass, T actualImplementation, T secondImplementation) { this.actualImplementation = actualImplementation; this.secondImplementation = secondImplementation; this.interfaceClass = interfaceClass; } /** * Returns the proxy to the actualImplementation. * *

The return value is a drop-in replacement to the actualImplementation, but will log any * difference with the secondImplementation during normal execution. */ public final T makeProxy() { return Reflection.newProxy(interfaceClass, this); } /** * Called when there was a difference between the implementations. * * @param method the method where the difference was found * @param message human readable description of the difference found */ protected abstract void log(Method method, String message); /** * Implements toString for specific types. * *

By default objects are logged using their .toString. If .toString isn't implemented for * some relevant classes (or if we want to use a different version), override this method with * the desired implementation. * * @param method the method whose return value is given * @param object the object returned by a call to method */ protected String stringifyResult( @SuppressWarnings("unused") Method method, @Nullable Object object) { return String.valueOf(object); } /** * Checks whether the method results are as similar as we expect. * *

By default objects are compared using their .equals. If .equals isn't implemented for some * relevant classes (or if we want change what is considered "not equal"), override this method * with the desired implementation. * * @param method the method whose return value is given * @param actual the object returned by a call to method for the "actual" implementation * @param second the object returned by a call to method for the "second" implementation */ protected boolean compareResults( @SuppressWarnings("unused") Method method, @Nullable Object actual, @Nullable Object second) { return Objects.equals(actual, second); } /** * Checks whether the thrown exceptions are as similar as we expect. * *

By default this returns 'true' for any input: all we check by default is that both * implementations threw something. Override if you need to actually compare both throwables. * * @param method the method whose return value is given * @param actual the exception thrown by a call to method for the "actual" implementation * @param second the exception thrown by a call to method for the "second" implementation */ protected boolean compareThrown( @SuppressWarnings("unused") Method method, Throwable actual, Throwable second) { return true; } /** * Implements toString for thrown exceptions. * *

By default exceptions are logged using their .toString. If more data is needed (part of * stack trace for example), override this method with the desired implementation. * * @param method the method whose return value is given * @param throwable the exception thrown by a call to method */ protected String stringifyThrown( @SuppressWarnings("unused") Method method, Throwable throwable) { return throwable.toString(); } @Override public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object actualResult = null; Throwable actualException = null; try { actualResult = method.invoke(actualImplementation, args); } catch (InvocationTargetException e) { actualException = e.getCause(); } Object secondResult = null; Throwable secondException = null; try { secondResult = method.invoke(secondImplementation, args); } catch (InvocationTargetException e) { secondException = e.getCause(); } // First compare the two implementations' result, and log any differences: if (actualException != null && secondException != null) { if (!compareThrown(method, actualException, secondException)) { log( method, String.format( "Both implementations threw, but got different exceptions! '%s' vs '%s'", stringifyThrown(method, actualException), stringifyThrown(method, secondException))); } } else if (actualException != null) { log( method, String.format( "Only actual implementation threw exception: %s", stringifyThrown(method, actualException))); } else if (secondException != null) { log( method, String.format( "Only second implementation threw exception: %s", stringifyThrown(method, secondException))); } else { // Neither threw exceptions - we compare the results if (!compareResults(method, actualResult, secondResult)) { log( method, String.format( "Got different results! '%s' vs '%s'", stringifyResult(method, actualResult), stringifyResult(method, secondResult))); } } // Now reproduce the actual implementation's behavior: if (actualException != null) { throw actualException; } return actualResult; } }