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 
TestThread(L lockLikeObject, String threadName)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")
tearDown()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    */
callAndAssertReturns(String methodName, Object... arguments)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    */
callAndAssertReturns(boolean expected, String methodName, Object... arguments)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    */
callAndAssertReturns(int expected, String methodName, Object... arguments)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    */
callAndAssertThrows(Class<? extends Throwable> expected, String methodName, Object... arguments)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    */
callAndAssertBlocks(String methodName, Object... arguments)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    */
callAndAssertWaits(String methodName, Object conditionLikeObject)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    */
assertPriorCallReturns(@ullable String methodName)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    */
assertPriorCallReturns(boolean expected, @Nullable String methodName)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    */
sendRequest(String methodName, Object... arguments)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    */
getResponse(String methodName)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 
invokeMethod(String methodName, Object... arguments)215   private Object invokeMethod(String methodName, Object... arguments) throws Exception {
216     return getMethod(methodName, arguments).invoke(lockLikeObject, arguments);
217   }
218 
getMethod(String methodName, Object... arguments)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 
run()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 
Request(String methodName, Object[] arguments)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 
Response(String methodName, Object result, Throwable throwable)278     Response(String methodName, Object result, Throwable throwable) {
279       this.methodName = methodName;
280       this.result = result;
281       this.throwable = throwable;
282     }
283 
getResult()284     Object getResult() {
285       if (throwable != null) {
286         throw (AssertionFailedError) new AssertionFailedError().initCause(throwable);
287       }
288       return result;
289     }
290 
getThrowable()291     Throwable getThrowable() {
292       assertNotNull(throwable);
293       return throwable;
294     }
295   }
296 }
297