1 /* 2 * Copyright (C) 2018 The Android Open Source Project 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 android.webkit.cts; 18 19 import android.os.Handler; 20 import android.os.Looper; 21 22 import com.google.common.util.concurrent.SettableFuture; 23 24 import java.util.concurrent.BlockingQueue; 25 import java.util.concurrent.Callable; 26 import java.util.concurrent.ExecutionException; 27 import java.util.concurrent.Future; 28 import java.util.concurrent.TimeUnit; 29 import java.util.concurrent.TimeoutException; 30 31 /** 32 * Helper methods for common webkit test tasks. 33 * 34 * <p> 35 * This should remain functionally equivalent to androidx.webkit.WebkitUtils. 36 * Modifications to this class should be reflected in that class as necessary. See 37 * http://go/modifying-webview-cts. 38 */ 39 public final class WebkitUtils { 40 41 /** 42 * Arbitrary timeout for tests. This is intended to be used with {@link TimeUnit#MILLISECONDS} 43 * so that this can represent 20 seconds. 44 * 45 * <p class=note><b>Note:</b> only use this timeout value for the unexpected case, not for the 46 * correct case, as this exceeds the time recommendation for {@link 47 * androidx.test.filters.MediumTest}. 48 */ 49 public static final long TEST_TIMEOUT_MS = 20000L; // 20s. 50 51 // A handler for the main thread. 52 private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); 53 54 /** 55 * Executes a callable asynchronously on the main thread, returning a future for the result. 56 * 57 * @param callable the {@link Callable} to execute. 58 * @return a {@link Future} representing the result of {@code callable}. 59 */ onMainThread(final Callable<T> callable)60 public static <T> Future<T> onMainThread(final Callable<T> callable) { 61 final SettableFuture<T> future = SettableFuture.create(); 62 sMainHandler.post(new Runnable() { 63 @Override 64 public void run() { 65 try { 66 future.set(callable.call()); 67 } catch (Throwable t) { 68 future.setException(t); 69 } 70 } 71 }); 72 return future; 73 } 74 75 /** 76 * Executes a runnable asynchronously on the main thread. 77 * 78 * @param runnable the {@link Runnable} to execute. 79 */ onMainThread(final Runnable runnable)80 public static void onMainThread(final Runnable runnable) { 81 sMainHandler.post(runnable); 82 } 83 84 /** 85 * Executes a callable synchronously on the main thread, returning its result. This re-throws 86 * any exceptions on the thread this is called from. This means callers may use {@link 87 * org.junit.Assert} methods within the {@link Callable} if they invoke this method from the 88 * instrumentation thread. 89 * 90 * <p class="note"><b>Note:</b> this should not be called from the UI thread. 91 * 92 * @param callable the {@link Callable} to execute. 93 * @return the result of the {@link Callable}. 94 */ onMainThreadSync(final Callable<T> callable)95 public static <T> T onMainThreadSync(final Callable<T> callable) { 96 if (Looper.myLooper() == Looper.getMainLooper()) { 97 throw new IllegalStateException("This cannot be called from the UI thread."); 98 } 99 return waitForFuture(onMainThread(callable)); 100 } 101 102 /** 103 * Executes a {@link Runnable} synchronously on the main thread. This is similar to {@link 104 * android.app.Instrumentation#runOnMainSync(Runnable)}, with the main difference that this 105 * re-throws exceptions on the calling thread. This is useful if {@code runnable} contains any 106 * {@link org.junit.Assert} methods, or otherwise throws an Exception. 107 * 108 * <p class="note"><b>Note:</b> this should not be called from the UI thread. 109 * 110 * @param runnable the {@link Runnable} to execute. 111 */ onMainThreadSync(final Runnable runnable)112 public static void onMainThreadSync(final Runnable runnable) { 113 if (Looper.myLooper() == Looper.getMainLooper()) { 114 throw new IllegalStateException("This cannot be called from the UI thread."); 115 } 116 final SettableFuture<Void> exceptionPropagatingFuture = SettableFuture.create(); 117 onMainThread(new Runnable() { 118 @Override 119 public void run() { 120 try { 121 runnable.run(); 122 exceptionPropagatingFuture.set(null); 123 } catch (Throwable t) { 124 exceptionPropagatingFuture.setException(t); 125 } 126 } 127 }); 128 waitForFuture(exceptionPropagatingFuture); 129 } 130 131 132 /** 133 * Waits for {@code future} and returns its value (or times out). If {@code future} has an 134 * associated Exception, this will re-throw that Exception on the instrumentation thread 135 * (wrapping with an unchecked Exception if necessary, to avoid requiring callers to declare 136 * checked Exceptions). 137 * 138 * @param future the {@link Future} representing a value of interest. 139 * @return the value {@code future} represents. 140 */ waitForFuture(Future<T> future)141 public static <T> T waitForFuture(Future<T> future) { 142 try { 143 return future.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); 144 } catch (ExecutionException e) { 145 // ExecutionException means this Future has an associated Exception that we should 146 // re-throw on the current thread. We throw the cause instead of ExecutionException, 147 // since ExecutionException itself isn't interesting, and might mislead those debugging 148 // test failures to suspect this method is the culprit (whereas the root cause is from 149 // another thread). 150 Throwable cause = e.getCause(); 151 // If the cause is an unchecked Throwable type, re-throw as-is. 152 if (cause instanceof Error) throw (Error) cause; 153 if (cause instanceof RuntimeException) throw (RuntimeException) cause; 154 // Otherwise, wrap this in an unchecked Exception so callers don't need to declare 155 // checked Exceptions. 156 throw new RuntimeException(cause); 157 } catch (InterruptedException | TimeoutException e) { 158 // Don't call e.getCause() for either of these. Unlike ExecutionException, these don't 159 // wrap the root cause, but rather are themselves interesting. Again, we wrap these 160 // checked Exceptions with an unchecked Exception for the caller's convenience. 161 // 162 // Although we might be tempted to handle InterruptedException by calling 163 // Thread.currentThread().interrupt(), this is not correct in this case. The interrupted 164 // thread was likely a different thread than the current thread, so there's nothing 165 // special we need to do. 166 throw new RuntimeException(e); 167 } 168 } 169 170 /** 171 * Takes an element out of the {@link BlockingQueue} (or times out). 172 */ waitForNextQueueElement(BlockingQueue<T> queue)173 public static <T> T waitForNextQueueElement(BlockingQueue<T> queue) { 174 try { 175 T value = queue.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); 176 if (value == null) { 177 // {@code null} is the special value which means {@link BlockingQueue#poll} has 178 // timed out (also: there's no risk for collision with real values, because 179 // BlockingQueue does not allow null entries). Instead of returning this special 180 // value, let's throw a proper TimeoutException to stay consistent with {@link 181 // #waitForFuture}. 182 throw new TimeoutException( 183 "Timeout while trying to take next entry from BlockingQueue"); 184 } 185 return value; 186 } catch (TimeoutException | InterruptedException e) { 187 // Don't handle InterruptedException specially, since it indicates that a different 188 // Thread was interrupted, not this one. 189 throw new RuntimeException(e); 190 } 191 } 192 193 // Do not instantiate this class. WebkitUtils()194 private WebkitUtils() {} 195 } 196