• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.qual.Nullable;
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 @Nullable 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 new AssertionError("Uncaught throwable in " + getName(), uncaughtThrowable);
81     }
82   }
83 
84   /**
85    * Causes this thread to call the named void method, and asserts that the call returns normally.
86    */
callAndAssertReturns(String methodName, Object... arguments)87   public void callAndAssertReturns(String methodName, Object... arguments) throws Exception {
88     checkNotNull(methodName);
89     checkNotNull(arguments);
90     sendRequest(methodName, arguments);
91     assertSame(null, getResponse(methodName).getResult());
92   }
93 
94   /**
95    * Causes this thread to call the named method, and asserts that the call returns the expected
96    * boolean value.
97    */
callAndAssertReturns(boolean expected, String methodName, Object... arguments)98   public void callAndAssertReturns(boolean expected, String methodName, Object... arguments)
99       throws Exception {
100     checkNotNull(methodName);
101     checkNotNull(arguments);
102     sendRequest(methodName, arguments);
103     assertEquals(expected, getResponse(methodName).getResult());
104   }
105 
106   /**
107    * Causes this thread to call the named method, and asserts that the call returns the expected int
108    * value.
109    */
callAndAssertReturns(int expected, String methodName, Object... arguments)110   public void callAndAssertReturns(int expected, String methodName, Object... arguments)
111       throws Exception {
112     checkNotNull(methodName);
113     checkNotNull(arguments);
114     sendRequest(methodName, arguments);
115     assertEquals(expected, getResponse(methodName).getResult());
116   }
117 
118   /**
119    * Causes this thread to call the named method, and asserts that the call throws the expected type
120    * of throwable.
121    */
callAndAssertThrows( Class<? extends Throwable> expected, String methodName, Object... arguments)122   public void callAndAssertThrows(
123       Class<? extends Throwable> expected, String methodName, Object... arguments)
124       throws Exception {
125     checkNotNull(expected);
126     checkNotNull(methodName);
127     checkNotNull(arguments);
128     sendRequest(methodName, arguments);
129     assertEquals(expected, getResponse(methodName).getThrowable().getClass());
130   }
131 
132   /**
133    * Causes this thread to call the named method, and asserts that this thread becomes blocked on
134    * the lock-like object. The lock-like object must have a method equivalent to {@link
135    * java.util.concurrent.locks.ReentrantLock#hasQueuedThread(Thread)}.
136    */
callAndAssertBlocks(String methodName, Object... arguments)137   public void callAndAssertBlocks(String methodName, Object... arguments) throws Exception {
138     checkNotNull(methodName);
139     checkNotNull(arguments);
140     assertEquals(false, invokeMethod("hasQueuedThread", this));
141     sendRequest(methodName, arguments);
142     Thread.sleep(DUE_DILIGENCE_MILLIS);
143     assertEquals(true, invokeMethod("hasQueuedThread", this));
144     assertNull(responseQueue.poll());
145   }
146 
147   /**
148    * Causes this thread to call the named method, and asserts that this thread thereby waits on the
149    * given condition-like object. The lock-like object must have a method equivalent to {@link
150    * java.util.concurrent.locks.ReentrantLock#hasWaiters(java.util.concurrent.locks.Condition)},
151    * except that the method parameter must accept whatever condition-like object is passed into this
152    * method.
153    */
callAndAssertWaits(String methodName, Object conditionLikeObject)154   public void callAndAssertWaits(String methodName, Object conditionLikeObject) throws Exception {
155     checkNotNull(methodName);
156     checkNotNull(conditionLikeObject);
157     // TODO: Restore the following line when Monitor.hasWaiters() no longer acquires the lock.
158     // assertEquals(false, invokeMethod("hasWaiters", conditionLikeObject));
159     sendRequest(methodName, conditionLikeObject);
160     Thread.sleep(DUE_DILIGENCE_MILLIS);
161     assertEquals(true, invokeMethod("hasWaiters", conditionLikeObject));
162     assertNull(responseQueue.poll());
163   }
164 
165   /**
166    * Asserts that a prior call that had caused this thread to block or wait has since returned
167    * normally.
168    */
assertPriorCallReturns(@ullable String methodName)169   public void assertPriorCallReturns(@Nullable String methodName) throws Exception {
170     assertEquals(null, getResponse(methodName).getResult());
171   }
172 
173   /**
174    * Asserts that a prior call that had caused this thread to block or wait has since returned the
175    * expected boolean value.
176    */
assertPriorCallReturns(boolean expected, @Nullable String methodName)177   public void assertPriorCallReturns(boolean expected, @Nullable String methodName)
178       throws Exception {
179     assertEquals(expected, getResponse(methodName).getResult());
180   }
181 
182   /**
183    * Sends the given method call to this thread.
184    *
185    * @throws TimeoutException if this thread does not accept the request within a reasonable amount
186    *     of time
187    */
sendRequest(String methodName, Object... arguments)188   private void sendRequest(String methodName, Object... arguments) throws Exception {
189     if (!requestQueue.offer(
190         new Request(methodName, arguments), TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
191       throw new TimeoutException();
192     }
193   }
194 
195   /**
196    * Receives a response from this thread.
197    *
198    * @throws TimeoutException if this thread does not offer a response within a reasonable amount of
199    *     time
200    * @throws AssertionFailedError if the given method name does not match the name of the method
201    *     this thread has called most recently
202    */
getResponse(String methodName)203   private Response getResponse(String methodName) throws Exception {
204     Response response = responseQueue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
205     if (response == null) {
206       throw new TimeoutException();
207     }
208     assertEquals(methodName, response.methodName);
209     return response;
210   }
211 
invokeMethod(String methodName, Object... arguments)212   private Object invokeMethod(String methodName, Object... arguments) throws Exception {
213     return getMethod(methodName, arguments).invoke(lockLikeObject, arguments);
214   }
215 
getMethod(String methodName, Object... arguments)216   private Method getMethod(String methodName, Object... arguments) throws Exception {
217     METHODS:
218     for (Method method : lockLikeObject.getClass().getMethods()) {
219       Class<?>[] parameterTypes = method.getParameterTypes();
220       if (method.getName().equals(methodName) && (parameterTypes.length == arguments.length)) {
221         for (int i = 0; i < arguments.length; i++) {
222           if (!parameterTypes[i].isAssignableFrom(arguments[i].getClass())) {
223             continue METHODS;
224           }
225         }
226         return method;
227       }
228     }
229     throw new NoSuchMethodError(methodName);
230   }
231 
232   @Override
run()233   public void run() {
234     assertSame(this, Thread.currentThread());
235     try {
236       while (true) {
237         Request request = requestQueue.take();
238         Object result;
239         try {
240           result = invokeMethod(request.methodName, request.arguments);
241         } catch (ThreadDeath death) {
242           return;
243         } catch (InvocationTargetException exception) {
244           responseQueue.put(new Response(request.methodName, null, exception.getTargetException()));
245           continue;
246         } catch (Throwable throwable) {
247           responseQueue.put(new Response(request.methodName, null, throwable));
248           continue;
249         }
250         responseQueue.put(new Response(request.methodName, result, null));
251       }
252     } catch (ThreadDeath death) {
253       return;
254     } catch (InterruptedException ignored) {
255       // SynchronousQueue sometimes throws InterruptedException while the threads are stopping.
256     } catch (Throwable uncaught) {
257       this.uncaughtThrowable = uncaught;
258     }
259   }
260 
261   private static class Request {
262     final String methodName;
263     final Object[] arguments;
264 
Request(String methodName, Object[] arguments)265     Request(String methodName, Object[] arguments) {
266       this.methodName = checkNotNull(methodName);
267       this.arguments = checkNotNull(arguments);
268     }
269   }
270 
271   private static class Response {
272     final String methodName;
273     final Object result;
274     final Throwable throwable;
275 
Response(String methodName, @Nullable Object result, @Nullable Throwable throwable)276     Response(String methodName, @Nullable Object result, @Nullable Throwable throwable) {
277       this.methodName = methodName;
278       this.result = result;
279       this.throwable = throwable;
280     }
281 
getResult()282     Object getResult() {
283       if (throwable != null) {
284         throw new AssertionError(throwable);
285       }
286       return result;
287     }
288 
getThrowable()289     Throwable getThrowable() {
290       assertNotNull(throwable);
291       return throwable;
292     }
293   }
294 }
295