1 /* 2 * Copyright 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 androidx.webkit.test.common; 18 19 import android.content.Context; 20 import android.os.Handler; 21 import android.os.Looper; 22 23 import androidx.concurrent.futures.ResolvableFuture; 24 import androidx.webkit.WebViewFeature; 25 26 import com.google.common.util.concurrent.ListenableFuture; 27 28 import org.jspecify.annotations.NonNull; 29 import org.junit.Assume; 30 31 import java.io.File; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.nio.charset.StandardCharsets; 35 import java.util.concurrent.BlockingQueue; 36 import java.util.concurrent.Callable; 37 import java.util.concurrent.ExecutionException; 38 import java.util.concurrent.Future; 39 import java.util.concurrent.TimeUnit; 40 import java.util.concurrent.TimeoutException; 41 42 /** 43 * Helper methods for common webkit test tasks. 44 * 45 * <p> 46 * This should remain functionally equivalent to android.webkit.cts.WebkitUtils. 47 * Modifications to this class should be reflected in that class as necessary. See 48 * http://go/modifying-webview-cts. 49 */ 50 public final class WebkitUtils { 51 52 /** 53 * Arbitrary timeout for tests. This is intended to be used with {@link TimeUnit#MILLISECONDS} 54 * so that this can represent 20 seconds. 55 * 56 * <p class=note><b>Note:</b> only use this timeout value for the unexpected case, not for the 57 * correct case, as this exceeds the time recommendation for {@link 58 * androidx.test.filters.MediumTest}. 59 */ 60 public static final long TEST_TIMEOUT_MS = 20000L; // 20s. 61 62 // A handler for the main thread. 63 private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); 64 65 /** 66 * Executes a callable asynchronously on the main thread, returning a future for the result. 67 * 68 * @param callable the {@link Callable} to execute. 69 * @return a {@link ListenableFuture} representing the result of {@code callable}. 70 */ onMainThread( final @NonNull Callable<T> callable)71 public static <T> @NonNull ListenableFuture<T> onMainThread( 72 final @NonNull Callable<T> callable) { 73 return onMainThreadDelayed(0, callable); 74 } 75 76 /** 77 * Executes a runnable asynchronously on the main thread. 78 * 79 * @param runnable the {@link Runnable} to execute. 80 */ onMainThread(final @NonNull Runnable runnable)81 public static void onMainThread(final @NonNull Runnable runnable) { 82 onMainThreadDelayed(0, runnable); 83 } 84 85 /** 86 * Executes a callable on the main thread after a delay, returning a future for the result. 87 * 88 * @param delayMs the delay in milliseconds 89 * @param callable the {@link Callable} to execute. 90 * @return a {@link ListenableFuture} representing the result of {@code callable}. 91 */ onMainThreadDelayed( long delayMs, final @NonNull Callable<T> callable)92 public static <T> @NonNull ListenableFuture<T> onMainThreadDelayed( 93 long delayMs, final @NonNull Callable<T> callable) { 94 final ResolvableFuture<T> future = ResolvableFuture.create(); 95 sMainHandler.postDelayed(() -> { 96 try { 97 future.set(callable.call()); 98 } catch (Throwable t) { 99 future.setException(t); 100 } 101 }, delayMs); 102 return future; 103 } 104 105 /** 106 * Executes a runnable asynchronously on the main thread after a delay. 107 * 108 * @param delayMs the delay in milliseconds 109 * @param runnable the {@link Runnable} to execute. 110 */ onMainThreadDelayed(long delayMs, final @NonNull Runnable runnable)111 public static void onMainThreadDelayed(long delayMs, final @NonNull Runnable runnable) { 112 sMainHandler.postDelayed(runnable, delayMs); 113 } 114 115 /** 116 * Executes a callable synchronously on the main thread, returning its result. This re-throws 117 * any exceptions on the thread this is called from. This means callers may use {@link 118 * org.junit.Assert} methods within the {@link Callable} if they invoke this method from the 119 * instrumentation thread. 120 * 121 * <p class="note"><b>Note:</b> this should not be called from the UI thread. 122 * 123 * @param callable the {@link Callable} to execute. 124 * @return the result of the {@link Callable}. 125 */ onMainThreadSync(final @NonNull Callable<T> callable)126 public static <T> T onMainThreadSync(final @NonNull Callable<T> callable) { 127 if (Looper.myLooper() == Looper.getMainLooper()) { 128 throw new IllegalStateException("This cannot be called from the UI thread."); 129 } 130 return waitForFuture(onMainThread(callable)); 131 } 132 133 /** 134 * Executes a {@link Runnable} synchronously on the main thread. This is similar to {@link 135 * android.app.Instrumentation#runOnMainSync(Runnable)}, with the main difference that this 136 * re-throws exceptions on the calling thread. This is useful if {@code runnable} contains any 137 * {@link org.junit.Assert} methods, or otherwise throws an Exception. 138 * 139 * <p class="note"><b>Note:</b> this should not be called from the UI thread. 140 * 141 * @param runnable the {@link Runnable} to execute. 142 */ onMainThreadSync(final @NonNull Runnable runnable)143 public static void onMainThreadSync(final @NonNull Runnable runnable) { 144 if (Looper.myLooper() == Looper.getMainLooper()) { 145 throw new IllegalStateException("This cannot be called from the UI thread."); 146 } 147 final ResolvableFuture<Void> exceptionPropagatingFuture = ResolvableFuture.create(); 148 onMainThread(() -> { 149 try { 150 runnable.run(); 151 exceptionPropagatingFuture.set(null); 152 } catch (Throwable t) { 153 exceptionPropagatingFuture.setException(t); 154 } 155 }); 156 waitForFuture(exceptionPropagatingFuture); 157 } 158 159 /** 160 * Throws {@link org.junit.AssumptionViolatedException} if the device does not support the 161 * particular feature, otherwise returns. 162 * 163 * <p> 164 * This provides a more descriptive error message than a bare {@code assumeTrue} call. 165 * 166 * <p> 167 * Note that this method is AndroidX-specific, and is not reflected in the CTS class. 168 * 169 * @param featureName the feature to be checked 170 */ checkFeature(@onNull String featureName)171 public static void checkFeature(@NonNull String featureName) { 172 final String msg = "This device does not have the feature '" + featureName + "'"; 173 final boolean hasFeature = WebViewFeature.isFeatureSupported(featureName); 174 Assume.assumeTrue(msg, hasFeature); 175 } 176 177 /** 178 * Throws {@link org.junit.AssumptionViolatedException} if the device does not support the 179 * particular feature, otherwise returns. 180 * 181 * <p> 182 * This provides a more descriptive error message than a bare {@code assumeTrue} call. 183 * 184 * <p> 185 * Note that this method is AndroidX-specific, and is not reflected in the CTS class. 186 * 187 * @param featureName the feature to be checked 188 */ checkStartupFeature(@onNull Context ctx, @NonNull String featureName)189 public static void checkStartupFeature(@NonNull Context ctx, @NonNull String featureName) { 190 final String msg = "This device does not have the startup feature '" + featureName + "'"; 191 final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(ctx, featureName); 192 Assume.assumeTrue(msg, hasFeature); 193 } 194 195 196 /** 197 * Waits for {@code future} and returns its value (or times out). If {@code future} has an 198 * associated Exception, this will re-throw that Exception on the instrumentation thread 199 * (wrapping with an unchecked Exception if necessary, to avoid requiring callers to declare 200 * checked Exceptions). 201 * 202 * @param future the {@link Future} representing a value of interest. 203 * @return the value {@code future} represents. 204 */ waitForFuture(@onNull Future<T> future)205 public static <T> T waitForFuture(@NonNull Future<T> future) { 206 try { 207 return future.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); 208 } catch (ExecutionException e) { 209 // ExecutionException means this Future has an associated Exception that we should 210 // re-throw on the current thread. We throw the cause instead of ExecutionException, 211 // since ExecutionException itself isn't interesting, and might mislead those debugging 212 // test failures to suspect this method is the culprit (whereas the root cause is from 213 // another thread). 214 Throwable cause = e.getCause(); 215 // If the cause is an unchecked Throwable type, re-throw as-is. 216 if (cause instanceof Error) throw (Error) cause; 217 if (cause instanceof RuntimeException) throw (RuntimeException) cause; 218 // Otherwise, wrap this in an unchecked Exception so callers don't need to declare 219 // checked Exceptions. 220 throw new RuntimeException(cause); 221 } catch (InterruptedException | TimeoutException e) { 222 // Don't call e.getCause() for either of these. Unlike ExecutionException, these don't 223 // wrap the root cause, but rather are themselves interesting. Again, we wrap these 224 // checked Exceptions with an unchecked Exception for the caller's convenience. 225 // 226 // Although we might be tempted to handle InterruptedException by calling 227 // Thread.currentThread().interrupt(), this is not correct in this case. The interrupted 228 // thread was likely a different thread than the current thread, so there's nothing 229 // special we need to do. 230 throw new RuntimeException(e); 231 } 232 } 233 234 /** 235 * Takes an element out of the {@link BlockingQueue} (or times out). 236 */ waitForNextQueueElement(@onNull BlockingQueue<T> queue)237 public static <T> T waitForNextQueueElement(@NonNull BlockingQueue<T> queue) { 238 try { 239 T value = queue.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); 240 if (value == null) { 241 // {@code null} is the special value which means {@link BlockingQueue#poll} has 242 // timed out (also: there's no risk for collision with real values, because 243 // BlockingQueue does not allow null entries). Instead of returning this special 244 // value, let's throw a proper TimeoutException to stay consistent with {@link 245 // #waitForFuture}. 246 throw new TimeoutException( 247 "Timeout while trying to take next entry from BlockingQueue"); 248 } 249 return value; 250 } catch (TimeoutException | InterruptedException e) { 251 // Don't handle InterruptedException specially, since it indicates that a different 252 // Thread was interrupted, not this one. 253 throw new RuntimeException(e); 254 } 255 } 256 257 /** 258 * Write a string to a file, and create the whole parent directories if they don't exist. 259 */ writeToFile(@onNull File file, @NonNull String content)260 public static void writeToFile(@NonNull File file, @NonNull String content) 261 throws IOException { 262 file.getParentFile().mkdirs(); 263 try (FileOutputStream fos = new FileOutputStream(file)) { 264 fos.write(content.getBytes(StandardCharsets.UTF_8)); 265 } 266 } 267 268 /** 269 * Delete the given File and (if it's a directory) everything within it. 270 * 271 * @param currentFile The file or directory to delete. Does not need to exist. 272 * @return Whether currentFile does not exist afterwards. 273 */ recursivelyDeleteFile(@onNull File currentFile)274 public static boolean recursivelyDeleteFile(@NonNull File currentFile) { 275 if (!currentFile.exists()) { 276 return true; 277 } 278 if (currentFile.isDirectory()) { 279 File[] files = currentFile.listFiles(); 280 if (files != null) { 281 for (File file : files) { 282 recursivelyDeleteFile(file); 283 } 284 } 285 } 286 287 return currentFile.delete(); 288 } 289 290 /** 291 * Check if the given looper is the current thread. 292 * 293 * <p> 294 * Note that this method is AndroidX-specific, and is not reflected in the CTS class. 295 * 296 * Backwards-compatible implementation of {@link Looper#isCurrentThread()} 297 * 298 * @return {@code true} if the current thread is the loopers thread 299 */ isCurrentThread(@onNull Looper looper)300 public static boolean isCurrentThread(@NonNull Looper looper) { 301 return Thread.currentThread().equals(looper.getThread()); 302 } 303 304 // Do not instantiate this class. WebkitUtils()305 private WebkitUtils() { 306 } 307 } 308