/
githubmirror
/
guava
Обзор
Документация
Войти
/
githubmirror
/
guava
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
android/guava-tests/test/com/google/common/util/concurrent/TestThread.java
314 строк
12 KB
cpovirk
Adapt to the once and future TODO style.
28 июл 2026, 23:37
28 июл 2026, 23:37
3b1c731
Код
Авторство
О чём код?
/* * 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 com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static junit.framework.Assert.assertEquals; import com.google.common.annotations.GwtIncompatible; import com.google.common.annotations.J2ktIncompatible; 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.TimeoutException; import junit.framework.AssertionFailedError; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; /** * 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 */ @NullUnmarked @GwtIncompatible @J2ktIncompatible 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<>(); private final SynchronousQueue<Response> responseQueue = new SynchronousQueue<>(); private @Nullable Throwable uncaughtThrowable = null; public TestThread(L lockLikeObject, String threadName) { super(threadName); this.lockLikeObject = checkNotNull(lockLikeObject); start(); } /* * TODO(b/318391980): Once we test only under Java 20 and higher, avoid calling Thread.stop. As * of Java 20, it always throws an exception, and as of Java 26, the method does not even exist. * For now, we continue using it to clean up under older JDKs. * * Our usages should at least be *relatively* safe: Typically, 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 other cases, a test may put a thread into an uninterruptible operation * intentionally, so there is no other way to clean up these threads. (The better solution, * though, would be to run the tests that use TestThread in separate VMs so that their threads * don't hang around during other tests.) */ @Override public void tearDown() throws Exception { try { Thread.class.getMethod("stop").invoke(this); join(); } catch (ReflectiveOperationException e) { // stop() threw or did not exist. Don't join() the thread, which might hang forever. } if (uncaughtThrowable != null) { throw new AssertionError("Uncaught throwable in " + getName(), 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); assertThat(getResponse(methodName).getResult()).isNull(); } /** * 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); assertThat(getResponse(methodName).getThrowable()).isInstanceOf(expected); } /** * 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)); assertThat(responseQueue.poll()).isNull(); } /** * 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)); assertThat(responseQueue.poll()).isNull(); } /** * 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 { assertThat(getResponse(methodName).getResult()).isNull(); } /** * 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, 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, MILLISECONDS); if (response == null) { throw new TimeoutException(); } assertThat(response.methodName).isEqualTo(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) { 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() { assertThat(Thread.currentThread()).isEqualTo(this); try { while (true) { Request request = requestQueue.take(); Object result; try { result = invokeMethod(request.methodName, request.arguments); } catch (InvocationTargetException exception) { responseQueue.put(new Response(request.methodName, null, exception.getTargetException())); continue; } catch (Throwable throwable) { if (isThreadDeath(throwable)) { return; } responseQueue.put(new Response(request.methodName, null, throwable)); continue; } responseQueue.put(new Response(request.methodName, result, null)); } } catch (InterruptedException ignored) { // SynchronousQueue sometimes throws InterruptedException while the threads are stopping. } catch (Throwable uncaught) { if (isThreadDeath(uncaught)) { return; } this.uncaughtThrowable = uncaught; } } private static boolean isThreadDeath(Throwable t) { return t.getClass().getName().equals("java.lang.ThreadDeath"); } private static final class Request { final String methodName; final Object[] arguments; Request(String methodName, Object[] arguments) { this.methodName = checkNotNull(methodName); this.arguments = checkNotNull(arguments); } } private static final class Response { final String methodName; final @Nullable Object result; final @Nullable Throwable throwable; Response(String methodName, @Nullable Object result, @Nullable Throwable throwable) { this.methodName = methodName; this.result = result; this.throwable = throwable; } @Nullable Object getResult() { if (throwable != null) { throw new AssertionError(throwable); } return result; } Throwable getThrowable() { assertThat(throwable).isNotNull(); return throwable; } } }