Home | History | Annotate | Download | only in concurrent
      1 /*
      2  * Copyright (C) 2010 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.util.concurrent;
     18 
     19 import static com.google.common.base.Preconditions.checkNotNull;
     20 import static junit.framework.Assert.assertEquals;
     21 import static junit.framework.Assert.assertNotNull;
     22 import static junit.framework.Assert.assertNull;
     23 import static junit.framework.Assert.assertSame;
     24 
     25 import com.google.common.testing.TearDown;
     26 
     27 import junit.framework.AssertionFailedError;
     28 
     29 import java.lang.reflect.InvocationTargetException;
     30 import java.lang.reflect.Method;
     31 import java.util.concurrent.SynchronousQueue;
     32 import java.util.concurrent.TimeUnit;
     33 import java.util.concurrent.TimeoutException;
     34 
     35 import javax.annotation.Nullable;
     36 
     37 /**
     38  * A helper for concurrency testing. One or more {@code TestThread} instances are instantiated
     39  * in a test with reference to the same "lock-like object", and then their interactions with that
     40  * object are choreographed via the various methods on this class.
     41  *
     42  * <p>A "lock-like object" is really any object that may be used for concurrency control. If the
     43  * {@link #callAndAssertBlocks} method is ever called in a test, the lock-like object must have a
     44  * method equivalent to {@link java.util.concurrent.locks.ReentrantLock#hasQueuedThread(Thread)}. If
     45  * the {@link #callAndAssertWaits} method is ever called in a test, the lock-like object must have a
     46  * method equivalent to {@link
     47  * java.util.concurrent.locks.ReentrantLock#hasWaiters(java.util.concurrent.locks.Condition)},
     48  * except that the method parameter must accept whatever condition-like object is passed into
     49  * {@code callAndAssertWaits} by the test.
     50  *
     51  * @param <L> the type of the lock-like object to be used
     52  * @author Justin T. Sampson
     53  */
     54 public final class TestThread<L> extends Thread implements TearDown {
     55 
     56   private static final long DUE_DILIGENCE_MILLIS = 50;
     57   private static final long TIMEOUT_MILLIS = 5000;
     58 
     59   private final L lockLikeObject;
     60 
     61   private final SynchronousQueue<Request> requestQueue = new SynchronousQueue<Request>();
     62   private final SynchronousQueue<Response> responseQueue = new SynchronousQueue<Response>();
     63 
     64   private Throwable uncaughtThrowable = null;
     65 
     66   public TestThread(L lockLikeObject, String threadName) {
     67     super(threadName);
     68     this.lockLikeObject = checkNotNull(lockLikeObject);
     69     start();
     70   }
     71 
     72   // Thread.stop() is okay because all threads started by a test are dying at the end of the test,
     73   // so there is no object state put at risk by stopping the threads abruptly. In some cases a test
     74   // may put a thread into an uninterruptible operation intentionally, so there is no other way to
     75   // clean up these threads.
     76   @SuppressWarnings("deprecation")
     77   @Override public void tearDown() throws Exception {
     78     stop();
     79     join();
     80 
     81     if (uncaughtThrowable != null) {
     82       throw (AssertionFailedError) new AssertionFailedError("Uncaught throwable in " + getName())
     83           .initCause(uncaughtThrowable);
     84     }
     85   }
     86 
     87   /**
     88    * Causes this thread to call the named void method, and asserts that the call returns normally.
     89    */
     90   public void callAndAssertReturns(String methodName, Object... arguments) throws Exception {
     91     checkNotNull(methodName);
     92     checkNotNull(arguments);
     93     sendRequest(methodName, arguments);
     94     assertSame(null, getResponse(methodName).getResult());
     95   }
     96 
     97   /**
     98    * Causes this thread to call the named method, and asserts that the call returns the expected
     99    * boolean value.
    100    */
    101   public void callAndAssertReturns(boolean expected, String methodName, Object... arguments)
    102       throws Exception {
    103     checkNotNull(methodName);
    104     checkNotNull(arguments);
    105     sendRequest(methodName, arguments);
    106     assertEquals(expected, getResponse(methodName).getResult());
    107   }
    108 
    109   /**
    110    * Causes this thread to call the named method, and asserts that the call returns the expected
    111    * int value.
    112    */
    113   public void callAndAssertReturns(int expected, String methodName, Object... arguments)
    114       throws Exception {
    115     checkNotNull(methodName);
    116     checkNotNull(arguments);
    117     sendRequest(methodName, arguments);
    118     assertEquals(expected, getResponse(methodName).getResult());
    119   }
    120 
    121   /**
    122    * Causes this thread to call the named method, and asserts that the call throws the expected
    123    * type of throwable.
    124    */
    125   public void callAndAssertThrows(Class<? extends Throwable> expected,
    126       String methodName, Object... arguments) throws Exception {
    127     checkNotNull(expected);
    128     checkNotNull(methodName);
    129     checkNotNull(arguments);
    130     sendRequest(methodName, arguments);
    131     assertEquals(expected, getResponse(methodName).getThrowable().getClass());
    132   }
    133 
    134   /**
    135    * Causes this thread to call the named method, and asserts that this thread becomes blocked on
    136    * the lock-like object. The lock-like object must have a method equivalent to {@link
    137    * java.util.concurrent.locks.ReentrantLock#hasQueuedThread(Thread)}.
    138    */
    139   public void callAndAssertBlocks(String methodName, Object... arguments) throws Exception {
    140     checkNotNull(methodName);
    141     checkNotNull(arguments);
    142     assertEquals(false, invokeMethod("hasQueuedThread", this));
    143     sendRequest(methodName, arguments);
    144     Thread.sleep(DUE_DILIGENCE_MILLIS);
    145     assertEquals(true, invokeMethod("hasQueuedThread", this));
    146     assertNull(responseQueue.poll());
    147   }
    148 
    149   /**
    150    * Causes this thread to call the named method, and asserts that this thread thereby waits on
    151    * the given condition-like object. The lock-like object must have a method equivalent to {@link
    152    * java.util.concurrent.locks.ReentrantLock#hasWaiters(java.util.concurrent.locks.Condition)},
    153    * except that the method parameter must accept whatever condition-like object is passed into
    154    * this method.
    155    */
    156   public void callAndAssertWaits(String methodName, Object conditionLikeObject)
    157       throws Exception {
    158     checkNotNull(methodName);
    159     checkNotNull(conditionLikeObject);
    160     // TODO: Restore the following line when Monitor.hasWaiters() no longer acquires the lock.
    161     // assertEquals(false, invokeMethod("hasWaiters", conditionLikeObject));
    162     sendRequest(methodName, conditionLikeObject);
    163     Thread.sleep(DUE_DILIGENCE_MILLIS);
    164     assertEquals(true, invokeMethod("hasWaiters", conditionLikeObject));
    165     assertNull(responseQueue.poll());
    166   }
    167 
    168   /**
    169    * Asserts that a prior call that had caused this thread to block or wait has since returned
    170    * normally.
    171    */
    172   public void assertPriorCallReturns(@Nullable String methodName) throws Exception {
    173     assertEquals(null, getResponse(methodName).getResult());
    174   }
    175 
    176   /**
    177    * Asserts that a prior call that had caused this thread to block or wait has since returned
    178    * the expected boolean value.
    179    */
    180   public void assertPriorCallReturns(boolean expected, @Nullable String methodName)
    181       throws Exception {
    182     assertEquals(expected, getResponse(methodName).getResult());
    183   }
    184 
    185   /**
    186    * Sends the given method call to this thread.
    187    *
    188    * @throws TimeoutException if this thread does not accept the request within a resonable amount
    189    *         of time
    190    */
    191   private void sendRequest(String methodName, Object... arguments) throws Exception {
    192     if (!requestQueue.offer(
    193         new Request(methodName, arguments), TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
    194       throw new TimeoutException();
    195     }
    196   }
    197 
    198   /**
    199    * Receives a response from this thread.
    200    *
    201    * @throws TimeoutException if this thread does not offer a response within a resonable amount of
    202    *         time
    203    * @throws AssertionFailedError if the given method name does not match the name of the method
    204    *         this thread has called most recently
    205    */
    206   private Response getResponse(String methodName) throws Exception {
    207     Response response = responseQueue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    208     if (response == null) {
    209       throw new TimeoutException();
    210     }
    211     assertEquals(methodName, response.methodName);
    212     return response;
    213   }
    214 
    215   private Object invokeMethod(String methodName, Object... arguments) throws Exception {
    216     return getMethod(methodName, arguments).invoke(lockLikeObject, arguments);
    217   }
    218 
    219   private Method getMethod(String methodName, Object... arguments) throws Exception {
    220     METHODS: for (Method method : lockLikeObject.getClass().getMethods()) {
    221       Class<?>[] parameterTypes = method.getParameterTypes();
    222       if (method.getName().equals(methodName) && (parameterTypes.length == arguments.length)) {
    223         for (int i = 0; i < arguments.length; i++) {
    224           if (!parameterTypes[i].isAssignableFrom(arguments[i].getClass())) {
    225             continue METHODS;
    226           }
    227         }
    228         return method;
    229       }
    230     }
    231     throw new NoSuchMethodError(methodName);
    232   }
    233 
    234   @Override public void run() {
    235     assertSame(this, Thread.currentThread());
    236     try {
    237       while (true) {
    238         Request request = requestQueue.take();
    239         Object result;
    240         try {
    241           result = invokeMethod(request.methodName, request.arguments);
    242         } catch (ThreadDeath death) {
    243           return;
    244         } catch (InvocationTargetException exception) {
    245           responseQueue.put(
    246               new Response(request.methodName, null, exception.getTargetException()));
    247           continue;
    248         } catch (Throwable throwable) {
    249           responseQueue.put(new Response(request.methodName, null, throwable));
    250           continue;
    251         }
    252         responseQueue.put(new Response(request.methodName, result, null));
    253       }
    254     } catch (ThreadDeath death) {
    255       return;
    256     } catch (InterruptedException ignored) {
    257       // SynchronousQueue sometimes throws InterruptedException while the threads are stopping.
    258     } catch (Throwable uncaught) {
    259       this.uncaughtThrowable = uncaught;
    260     }
    261   }
    262 
    263   private static class Request {
    264     final String methodName;
    265     final Object[] arguments;
    266 
    267     Request(String methodName, Object[] arguments) {
    268       this.methodName = checkNotNull(methodName);
    269       this.arguments = checkNotNull(arguments);
    270     }
    271   }
    272 
    273   private static class Response {
    274     final String methodName;
    275     final Object result;
    276     final Throwable throwable;
    277 
    278     Response(String methodName, Object result, Throwable throwable) {
    279       this.methodName = methodName;
    280       this.result = result;
    281       this.throwable = throwable;
    282     }
    283 
    284     Object getResult() {
    285       if (throwable != null) {
    286         throw (AssertionFailedError) new AssertionFailedError().initCause(throwable);
    287       }
    288       return result;
    289     }
    290 
    291     Throwable getThrowable() {
    292       assertNotNull(throwable);
    293       return throwable;
    294     }
    295   }
    296 }
    297