/* * Copyright (C) 2010 The Guava Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.util.concurrent; import static com.google.common.base.Preconditions.checkNotNull; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertSame; import com.google.common.testing.TearDown; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import junit.framework.AssertionFailedError; /** * A helper for concurrency testing. One or more {@code TestThread} instances are instantiated * in a test with reference to the same "lock-like object", and then their interactions with that * object are choreographed via the various methods on this class. * * <p>A "lock-like object" is really any object that may be used for concurrency control. If the * {@link #callAndAssertBlocks} method is ever called in a test, the lock-like object must have a * method equivalent to {@link java.util.concurrent.locks.ReentrantLock#hasQueuedThread(Thread)}. If * the {@link #callAndAssertWaits} method is ever called in a test, the lock-like object must have a * method equivalent to {@link * java.util.concurrent.locks.ReentrantLock#hasWaiters(java.util.concurrent.locks.Condition)}, * except that the method parameter must accept whatever condition-like object is passed into * {@code callAndAssertWaits} by the test. * * @param <L> the type of the lock-like object to be used * @author Justin T. Sampson */ public final class TestThread<L> extends Thread implements TearDown { private static final long DUE_DILIGENCE_MILLIS = 100; private static final long TIMEOUT_MILLIS = 5000; private final L lockLikeObject; private final SynchronousQueue<Request> requestQueue = new SynchronousQueue<Request>(); private final SynchronousQueue<Response> responseQueue = new SynchronousQueue<Response>(); private Throwable uncaughtThrowable = null; public TestThread(L lockLikeObject, String threadName) { super(threadName); this.lockLikeObject = checkNotNull(lockLikeObject); start(); } // Thread.stop() is okay because all threads started by a test are dying at the end of the test, // so there is no object state put at risk by stopping the threads abruptly. In some cases a test // may put a thread into an uninterruptible operation intentionally, so there is no other way to // clean up these threads. @SuppressWarnings("deprecation") @Override public void tearDown() throws Exception { stop(); join(); if (uncaughtThrowable != null) { throw (AssertionFailedError) new AssertionFailedError("Uncaught throwable in " + getName()) .initCause(uncaughtThrowable); } } /** * Causes this thread to call the named void method, and asserts that the call returns normally. */ public void callAndAssertReturns(String methodName, Object... arguments) throws Exception { checkNotNull(methodName); checkNotNull(arguments); sendRequest(methodName, arguments); assertSame(null, getResponse(methodName).getResult()); } /** * Causes this thread to call the named method, and asserts that the call returns the expected * boolean value. */ public void callAndAssertReturns(boolean expected, String methodName, Object... arguments) throws Exception { checkNotNull(methodName); checkNotNull(arguments); sendRequest(methodName, arguments); assertEquals(expected, getResponse(methodName).getResult()); } /** * Causes this thread to call the named method, and asserts that the call returns the expected * int value. */ public void callAndAssertReturns(int expected, String methodName, Object... arguments) throws Exception { checkNotNull(methodName); checkNotNull(arguments); sendRequest(methodName, arguments); assertEquals(expected, getResponse(methodName).getResult()); } /** * Causes this thread to call the named method, and asserts that the call throws the expected * type of throwable. */ public void callAndAssertThrows(Class<? extends Throwable> expected, String methodName, Object... arguments) throws Exception { checkNotNull(expected); checkNotNull(methodName); checkNotNull(arguments); sendRequest(methodName, arguments); assertEquals(expected, getResponse(methodName).getThrowable().getClass()); } /** * Causes this thread to call the named method, and asserts that this thread becomes blocked on * the lock-like object. The lock-like object must have a method equivalent to {@link * java.util.concurrent.locks.ReentrantLock#hasQueuedThread(Thread)}. */ public void callAndAssertBlocks(String methodName, Object... arguments) throws Exception { checkNotNull(methodName); checkNotNull(arguments); assertEquals(false, invokeMethod("hasQueuedThread", this)); sendRequest(methodName, arguments); Thread.sleep(DUE_DILIGENCE_MILLIS); assertEquals(true, invokeMethod("hasQueuedThread", this)); assertNull(responseQueue.poll()); } /** * Causes this thread to call the named method, and asserts that this thread thereby waits on * the given condition-like object. The lock-like object must have a method equivalent to {@link * java.util.concurrent.locks.ReentrantLock#hasWaiters(java.util.concurrent.locks.Condition)}, * except that the method parameter must accept whatever condition-like object is passed into * this method. */ public void callAndAssertWaits(String methodName, Object conditionLikeObject) throws Exception { checkNotNull(methodName); checkNotNull(conditionLikeObject); // TODO: Restore the following line when Monitor.hasWaiters() no longer acquires the lock. // assertEquals(false, invokeMethod("hasWaiters", conditionLikeObject)); sendRequest(methodName, conditionLikeObject); Thread.sleep(DUE_DILIGENCE_MILLIS); assertEquals(true, invokeMethod("hasWaiters", conditionLikeObject)); assertNull(responseQueue.poll()); } /** * Asserts that a prior call that had caused this thread to block or wait has since returned * normally. */ public void assertPriorCallReturns(@Nullable String methodName) throws Exception { assertEquals(null, getResponse(methodName).getResult()); } /** * Asserts that a prior call that had caused this thread to block or wait has since returned * the expected boolean value. */ public void assertPriorCallReturns(boolean expected, @Nullable String methodName) throws Exception { assertEquals(expected, getResponse(methodName).getResult()); } /** * Sends the given method call to this thread. * * @throws TimeoutException if this thread does not accept the request within a reasonable amount * of time */ private void sendRequest(String methodName, Object... arguments) throws Exception { if (!requestQueue.offer( new Request(methodName, arguments), TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { throw new TimeoutException(); } } /** * Receives a response from this thread. * * @throws TimeoutException if this thread does not offer a response within a reasonable amount of * time * @throws AssertionFailedError if the given method name does not match the name of the method * this thread has called most recently */ private Response getResponse(String methodName) throws Exception { Response response = responseQueue.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); if (response == null) { throw new TimeoutException(); } assertEquals(methodName, response.methodName); return response; } private Object invokeMethod(String methodName, Object... arguments) throws Exception { return getMethod(methodName, arguments).invoke(lockLikeObject, arguments); } private Method getMethod(String methodName, Object... arguments) throws Exception { METHODS: for (Method method : lockLikeObject.getClass().getMethods()) { Class<?>[] parameterTypes = method.getParameterTypes(); if (method.getName().equals(methodName) && (parameterTypes.length == arguments.length)) { for (int i = 0; i < arguments.length; i++) { if (!parameterTypes[i].isAssignableFrom(arguments[i].getClass())) { continue METHODS; } } return method; } } throw new NoSuchMethodError(methodName); } @Override public void run() { assertSame(this, Thread.currentThread()); try { while (true) { Request request = requestQueue.take(); Object result; try { result = invokeMethod(request.methodName, request.arguments); } catch (ThreadDeath death) { return; } catch (InvocationTargetException exception) { responseQueue.put( new Response(request.methodName, null, exception.getTargetException())); continue; } catch (Throwable throwable) { responseQueue.put(new Response(request.methodName, null, throwable)); continue; } responseQueue.put(new Response(request.methodName, result, null)); } } catch (ThreadDeath death) { return; } catch (InterruptedException ignored) { // SynchronousQueue sometimes throws InterruptedException while the threads are stopping. } catch (Throwable uncaught) { this.uncaughtThrowable = uncaught; } } private static class Request { final String methodName; final Object[] arguments; Request(String methodName, Object[] arguments) { this.methodName = checkNotNull(methodName); this.arguments = checkNotNull(arguments); } } private static class Response { final String methodName; final Object result; final Throwable throwable; Response(String methodName, Object result, Throwable throwable) { this.methodName = methodName; this.result = result; this.throwable = throwable; } Object getResult() { if (throwable != null) { throw (AssertionFailedError) new AssertionFailedError().initCause(throwable); } return result; } Throwable getThrowable() { assertNotNull(throwable); return throwable; } } }