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