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