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 import java.lang.reflect.InvocationTargetException;
27 import java.lang.reflect.Method;
28 import java.util.concurrent.SynchronousQueue;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31 import junit.framework.AssertionFailedError;
32 import org.checkerframework.checker.nullness.compatqual.NullableDecl;
33 
34 /**
35  * A helper for concurrency testing. One or more {@code TestThread} instances are instantiated in a
36  * test with reference to the same "lock-like object", and then their interactions with that object
37  * are choreographed via the various methods on this class.
38  *
39  * <p>A "lock-like object" is really any object that may be used for concurrency control. If the
40  * {@link #callAndAssertBlocks} method is ever called in a test, the lock-like object must have a
41  * method equivalent to {@link java.util.concurrent.locks.ReentrantLock#hasQueuedThread(Thread)}. If
42  * the {@link #callAndAssertWaits} method is ever called in a test, the lock-like object must have a
43  * method equivalent to {@link
44  * java.util.concurrent.locks.ReentrantLock#hasWaiters(java.util.concurrent.locks.Condition)},
45  * except that the method parameter must accept whatever condition-like object is passed into {@code
46  * callAndAssertWaits} by the test.
47  *
48  * @param <L> the type of the lock-like object to be used
49  * @author Justin T. Sampson
50  */
51 public final class TestThread<L> extends Thread implements TearDown {
52 
53   private static final long DUE_DILIGENCE_MILLIS = 100;
54   private static final long TIMEOUT_MILLIS = 5000;
55 
56   private final L lockLikeObject;
57 
58   private final SynchronousQueue<Request> requestQueue = new SynchronousQueue<>();
59   private final SynchronousQueue<Response> responseQueue = new SynchronousQueue<>();
60 
61   private Throwable uncaughtThrowable = null;
62 
TestThread(L lockLikeObject, String threadName)63   public TestThread(L lockLikeObject, String threadName) {
64     super(threadName);
65     this.lockLikeObject = checkNotNull(lockLikeObject);
66     start();
67   }
68 
69   // Thread.stop() is okay because all threads started by a test are dying at the end of the test,
70   // so there is no object state put at risk by stopping the threads abruptly. In some cases a test
71   // may put a thread into an uninterruptible operation intentionally, so there is no other way to
72   // clean up these threads.
73   @SuppressWarnings("deprecation")
74   @Override
tearDown()75   public void tearDown() throws Exception {
76     stop();
77     join();
78 
79     if (uncaughtThrowable != null) {
80       throw (AssertionFailedError)
81           new AssertionFailedError("Uncaught throwable in " + getName())
82               .initCause(uncaughtThrowable);
83     }
84   }
85 
86   /**
87    * Causes this thread to call the named void method, and asserts that the call returns normally.
88    */
callAndAssertReturns(String methodName, Object... arguments)89   public void callAndAssertReturns(String methodName, Object... arguments) throws Exception {
90     checkNotNull(methodName);
91     checkNotNull(arguments);
92     sendRequest(methodName, arguments);
93     assertSame(null, getResponse(methodName).getResult());
94   }
95 
96   /**
97    * Causes this thread to call the named method, and asserts that the call returns the expected
98    * boolean value.
99    */
callAndAssertReturns(boolean expected, String methodName, Object... arguments)100   public void callAndAssertReturns(boolean expected, String methodName, Object... arguments)
101       throws Exception {
102     checkNotNull(methodName);
103     checkNotNull(arguments);
104     sendRequest(methodName, arguments);
105     assertEquals(expected, getResponse(methodName).getResult());
106   }
107 
108   /**
109    * Causes this thread to call the named method, and asserts that the call returns the expected int
110    * value.
111    */
callAndAssertReturns(int expected, String methodName, Object... arguments)112   public void callAndAssertReturns(int expected, String methodName, Object... arguments)
113       throws Exception {
114     checkNotNull(methodName);
115     checkNotNull(arguments);
116     sendRequest(methodName, arguments);
117     assertEquals(expected, getResponse(methodName).getResult());
118   }
119 
120   /**
121    * Causes this thread to call the named method, and asserts that the call throws the expected type
122    * of throwable.
123    */
callAndAssertThrows( Class<? extends Throwable> expected, String methodName, Object... arguments)124   public void callAndAssertThrows(
125       Class<? extends Throwable> expected, String methodName, Object... arguments)
126       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 the
151    * 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 this
154    * method.
155    */
callAndAssertWaits(String methodName, Object conditionLikeObject)156   public void callAndAssertWaits(String methodName, Object conditionLikeObject) throws Exception {
157     checkNotNull(methodName);
158     checkNotNull(conditionLikeObject);
159     // TODO: Restore the following line when Monitor.hasWaiters() no longer acquires the lock.
160     // assertEquals(false, invokeMethod("hasWaiters", conditionLikeObject));
161     sendRequest(methodName, conditionLikeObject);
162     Thread.sleep(DUE_DILIGENCE_MILLIS);
163     assertEquals(true, invokeMethod("hasWaiters", conditionLikeObject));
164     assertNull(responseQueue.poll());
165   }
166 
167   /**
168    * Asserts that a prior call that had caused this thread to block or wait has since returned
169    * normally.
170    */
assertPriorCallReturns(@ullableDecl String methodName)171   public void assertPriorCallReturns(@NullableDecl String methodName) throws Exception {
172     assertEquals(null, getResponse(methodName).getResult());
173   }
174 
175   /**
176    * Asserts that a prior call that had caused this thread to block or wait has since returned the
177    * expected boolean value.
178    */
assertPriorCallReturns(boolean expected, @NullableDecl String methodName)179   public void assertPriorCallReturns(boolean expected, @NullableDecl String methodName)
180       throws Exception {
181     assertEquals(expected, getResponse(methodName).getResult());
182   }
183 
184   /**
185    * Sends the given method call to this thread.
186    *
187    * @throws TimeoutException if this thread does not accept the request within a reasonable amount
188    *     of time
189    */
sendRequest(String methodName, Object... arguments)190   private void sendRequest(String methodName, Object... arguments) throws Exception {
191     if (!requestQueue.offer(
192         new Request(methodName, arguments), TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
193       throw new TimeoutException();
194     }
195   }
196 
197   /**
198    * Receives a response from this thread.
199    *
200    * @throws TimeoutException if this thread does not offer a response within a reasonable amount of
201    *     time
202    * @throws AssertionFailedError if the given method name does not match the name of the method
203    *     this thread has called most recently
204    */
getResponse(String methodName)205   private Response getResponse(String methodName) throws Exception {
206     Response response = responseQueue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
207     if (response == null) {
208       throw new TimeoutException();
209     }
210     assertEquals(methodName, response.methodName);
211     return response;
212   }
213 
invokeMethod(String methodName, Object... arguments)214   private Object invokeMethod(String methodName, Object... arguments) throws Exception {
215     return getMethod(methodName, arguments).invoke(lockLikeObject, arguments);
216   }
217 
getMethod(String methodName, Object... arguments)218   private Method getMethod(String methodName, Object... arguments) throws Exception {
219     METHODS:
220     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
run()235   public void run() {
236     assertSame(this, Thread.currentThread());
237     try {
238       while (true) {
239         Request request = requestQueue.take();
240         Object result;
241         try {
242           result = invokeMethod(request.methodName, request.arguments);
243         } catch (ThreadDeath death) {
244           return;
245         } catch (InvocationTargetException exception) {
246           responseQueue.put(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