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