All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.testing.threadtester.MethodRecorder Maven / Gradle / Ivy

Go to download

Thread Weaver is a framework for writing multi-threaded unit tests in Java. It provides mechanisms for creating breakpoints within code, and for halting execution of a thread when a breakpoint is reached.

The newest version!
/*
 * Copyright 2009 Weaver authors
 *
 * 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 com.google.testing.threadtester;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

import java.lang.reflect.Method;

/**
 * Provides a mechanism for creating {@link CodePosition}s for a given
 * object-under-test, using direct method calls. Sample useage is as follows:
 * 
 *
 *  ClassUnderTest object = new ClassUnderTest();
 *  MethodRecorder recorder =
 *      new MethodRecorder(object)
 *
 *  ClassUnderTest control = recorder.getControl();
 *
 *  // Create a CodePosition corresponding to the start
 *  // of the call to 'aMethod' in the test object.
 *  CodePosition cp = recorder.atStart(control.aMethod()).position();
 * 
* * Calling {@link #getControl} returns a dummy object that is of the same class as * the object-under-test. Methods invoked on the control object have no effect, * but the method calls are noted by the MethodRecorder, and can then be used to * create CodePositions. If the methods invoked require arguments, any argument * (including null) may be passed in. *

* A CodePosition can also be created relative to an internal method call. Given * the following implementation: * *

 *  public ClassUnderTest {
 *    public int aMethod() {
 *      int value = getValue();
 *      System.out.printf("Value = %d", value);
 *      return value;
 *    }
 * 
* * A CodePosition in the method aMethod, immediately after the * call to printf, could be created as follows: * *
 *  ClassUnderTest control = recorder.getControl();
 *  PrintStream stream = recorder.createTarget(PrintStream.class);
 *  CodePosition cp =
 *    recorder.in(control.aMethod()).afterCalling(stream.printf("")).position();
 * 
* * Again, the target object is a dummy instance, and any argument may be passed * in to the method calls. *

* The above examples can only be used when the method in question returns a * value. If it does not, then the "LastMethod" variations must be used. Given * the following class: * *

 *  public ClassUnderTest {
 *    public void voidMethod() {
 *      System.out.println("in void method");
 *    }
 * 
* * A CodePosition in the method voidMethod, immediately after the * call to println, could be created as follows: * *
 *  ClassUnderTest control = recorder.getControl();
 *  PrintStream stream = recorder.createTarget(PrintStream.class);
 *  control.voidMethod();
 *  recorder.inLastMethod();
 *  stream.println("");
 *  recorder.afterCallingLastMethod();
 *  CodePosition cp = recorder.position();
 * 
* * @param the type for which we want to create positions. */ public class MethodRecorder { /** The instrumented object corresponding to the main object. */ private final ObjectInstrumentation instrumentedObject; /** The instrumented class corresponding to the instrumented object. */ private final ClassInstrumentation instrumentedClass; /** The control object. This is a dummy instance of the main object. */ private T controlObject; /** The last control method invoked. */ private volatile Method lastControlMethod; /** The last target method invoked. */ private volatile Method lastTargetMethod; /** The Objenesis factory for creating new control objects */ private Objenesis objenesis = new ObjenesisStd(); /** * The internal state. Represents the last recorded position in the control * object. */ private enum Position { /** At the start of a method call. */ START, /** At the end of a method call. */ END, /** * Within a method call. This is a transient state, and should normally * be followed by {@link #BEFORE_TARGET} or {@link #AFTER_TARGET}. */ WITHIN, /** Just before a call to a method on another object. */ BEFORE_TARGET, /** Just after a call to a method on another object. */ AFTER_TARGET, /** The intitial state, before any calls have been recorded. */ UNDEFINED } private volatile Position position = Position.UNDEFINED; /** * Intercepts any method invoked on the control or target objects, and records * the last method invoked. */ private abstract class Interceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // The invoked method doesn't actually do anything, it just records the // Method object. intercepted(method); return null; } /** * Intercepts a called method. */ abstract void intercepted(Method method); } /** * Creates a new instance of the given class, using the supplied interceptor. * Uses the EasyMock ClassInstantiatorFactory in order to avoid the cglib * limitation that prevents us from creating instances of classes that do not * have public default constructors. */ private Object create(Class clss, Interceptor interceptor) { Enhancer e = new Enhancer(); e.setSuperclass(clss); e.setCallbackType(interceptor.getClass()); Class controlClass = e.createClass(); Enhancer.registerCallbacks(controlClass, new Callback[] { interceptor }); Factory result = (Factory) objenesis.newInstance(controlClass); // This call is required to work around a cglib feature. See the comment in // org.easymock.classextension.internal.ClassProxyFactory, which uses the // same approach. result.getCallback(0); // And this call is required to work around a memory leak in cglib, which // sticks references to the class in a ThreadLocal that is never cleared. // See http://opensource.atlassian.com/projects/hibernate/browse/HHH-2481 Enhancer.registerCallbacks(controlClass, null); return result; } /** * Creates a new MethodRecorder for the given object. Note that the object's * class must be Instrumented. * * @see Instrumentation */ @SuppressWarnings("unchecked") public MethodRecorder(T object) { if (object == null) { throw new IllegalArgumentException("Main object cannot be null"); } // Get the instrumented object for the main object. This will verify // that the object's class is instrumented. instrumentedObject = Instrumentation.getObjectInstrumentation(object); instrumentedClass = Instrumentation.getClassInstrumentationForObject(object); initialize((Class) object.getClass()); } /** * Creates a new MethodRecorder for the given class. Note that the * class must be Instrumented. * * @see Instrumentation */ public MethodRecorder(Class clss) { if (clss == null) { throw new IllegalArgumentException("Class cannot be null"); } instrumentedObject = null; instrumentedClass = Instrumentation.getClassInstrumentation(clss); initialize(clss); } @SuppressWarnings("unchecked") private void initialize(Class clss) { Interceptor interceptor = new Interceptor() { @Override void intercepted(Method method) { lastControlMethod = method; } }; controlObject = (T) create(clss, interceptor); } /** * Gets the control object. This is a dummy instance of the object passed into * the constructor. Method calls made on the control object can be recorded to * create {@link CodePosition}s. */ @SuppressWarnings("unchecked") public T getControl() { return controlObject; } /** * Creates a target object of the given class. This is a dummy instance, and * can be used to record method calls. */ @SuppressWarnings("unchecked") public T createTarget(Class clss) { if (clss == null) { throw new IllegalArgumentException("Class cannot be null"); } Interceptor interceptor = new Interceptor() { @Override void intercepted(Method method) { lastTargetMethod = method; } }; return (T) create(clss, interceptor); } /** * Gets the instrumented object corresponding to the main object passed in to * the constructor. Returns null if this recorder was constructed using a * class, not an object. See {@link MethodRecorder#MethodRecorder(Class)} */ ObjectInstrumentation getInstrumentedObject() { return instrumentedObject; } /** * Gets the instrumented class corresponding to the instrumented object. * * @see #getInstrumentedObject */ ClassInstrumentation getInstrumentedClass() { return instrumentedClass; } /** * Sets the state of the recorder to represent a position within the last * method invoked on the control object. After calling this method, you must * call {@link #beforeCalling} or {@link #afterCalling} before calling {@link * #position}. * * @see #getControl */ public MethodRecorder in(Object result) { if (lastControlMethod == null) { throw new IllegalStateException("Must call a control method first"); } lastTargetMethod = null; position = Position.WITHIN; return this; } /** * Sets the state of the recorder to represent a position within the last * method invoked on the control object. Used for void methods where chaining * is impossible. * * @see #in */ public MethodRecorder inLastMethod() { return in(null); } /** * Sets the state of the recorder to represent a position at the beginning of * the last method invoked on the control object. */ public MethodRecorder atStartOf(Object result) { if (lastControlMethod == null) { throw new IllegalStateException("Must call a control method first"); } position = Position.START; return this; } /** * Sets the state of the recorder to represent a position at the beginning of * the last method invoked on the control object. Used for void methods where * chaining is impossible. */ public MethodRecorder atStartOfLastMethod() { return atStartOf(null); } /** * Sets the state of the recorder to represent a position at the end of * the last method invoked on the control object. */ public MethodRecorder atEndOf(Object result) { if (lastControlMethod == null) { throw new IllegalStateException("Must call a control method first"); } position = Position.END; return this; } /** * Sets the state of the recorder to represent a position at the end of the * last method invoked on the control object. Used for void methods where * chaining is impossible. */ public MethodRecorder atEndOfLastMethod() { return atEndOf(null); } /** * Sets the state of the recorder to represent a position before a call to the * last method invoked on the target object. You must call {@link #in} before * calling this method. */ public MethodRecorder beforeCalling(Object result) { if (position != Position.WITHIN) { throw new IllegalStateException("Must call a control method first"); } if (lastTargetMethod == null) { throw new IllegalStateException("Must call a target method first"); } position = Position.BEFORE_TARGET; return this; } /** * Sets the state of the recorder to represent a position before a call to the * last method invoked on the target object. Used for void methods where * chaining is impossible. * * @see #beforeCalling */ public MethodRecorder beforeCallingLastMethod() { return beforeCalling(null); } /** * Sets the state of the recorder to represent a position after a call to the * last method invoked on the target object. You must call {@link #in} before * calling this method. */ public MethodRecorder afterCalling(Object result) { if (position != Position.WITHIN) { throw new IllegalStateException("Must call a control method first"); } if (lastTargetMethod == null) { throw new IllegalStateException("Must call a target method first"); } position = Position.AFTER_TARGET; return this; } /** * Sets the state of the recorder to represent a position after a call to the * last method invoked on the target object. Used for void methods where * chaining is impossible. * * @see #afterCalling */ public MethodRecorder afterCallingLastMethod() { return afterCalling(null); } /** * Creates a new CodePosition corresponding to the last methods called on the * control object, and optionally on a target object. After returning a code * position, the state of the recorder is reset, and other target methods must * be invoked before calling this method again. * * @see #getControl * @see #createTarget * * @throws IllegalStateException if control and target methods have not been * called. */ public CodePosition position() { if (position == Position.UNDEFINED) { throw new IllegalStateException("No method has been called"); } return getPosition(); } /** * Creates a CodePosition if one has been defined. Returns null if no * target/control methods have been invoked. Will still throw * IllegalStateException if methods have been invoked incorrectly. * * @see #position */ CodePosition getPositionIfAny() { if (position == Position.UNDEFINED) { return null; } return getPosition(); } private CodePosition getPosition() { switch (position) { case START: case END: // We fall through to here - the check is the same for START and END if (lastTargetMethod != null) { throw new IllegalStateException("Cannot combine start/end with target object method"); } break; case WITHIN: // WITHIN is a transient state, and you need to call beforeCalling() or // afterCalling() first. throw new IllegalStateException("Must specify a target object method"); } try { switch (position) { case START: return instrumentedClass.atMethodStart(lastControlMethod); case END: return instrumentedClass.atMethodEnd(lastControlMethod); case BEFORE_TARGET: return instrumentedClass.beforeCall(lastControlMethod, lastTargetMethod); case AFTER_TARGET: return instrumentedClass.afterCall(lastControlMethod, lastTargetMethod); default: throw new IllegalStateException("Unknown state " + position); } } finally { position = Position.UNDEFINED; lastControlMethod = null; lastTargetMethod = null; } } /** * Creates a Breakpoint for the current CodePosition in the given thread. Note * that this method can only be called if this recorder was created with an * object, not a class. See {@link MethodRecorder#MethodRecorder(Class)} * * @see #position */ public Breakpoint breakpoint(Thread thread) { if (instrumentedObject == null) { throw new IllegalStateException( "Cannot get breakpoint unless recorder was created with an object"); } return instrumentedObject.createBreakpoint(position(), thread); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy