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