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 static org.hamcrest.MatcherAssert.assertThat;
20 import static org.hamcrest.Matchers.greaterThan;
21 
22 import android.test.ActivityInstrumentationTestCase2;
23 import android.webkit.TracingConfig;
24 import android.webkit.TracingController;
25 import android.webkit.WebView;
26 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
27 
28 import com.android.compatibility.common.util.NullWebViewUtils;
29 import com.android.compatibility.common.util.PollingCheck;
30 
31 import java.io.ByteArrayOutputStream;
32 import java.io.IOException;
33 import java.io.OutputStream;
34 import java.util.concurrent.atomic.AtomicInteger;
35 import java.util.concurrent.Callable;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.ExecutorService;
38 import java.util.concurrent.Executors;
39 import java.util.concurrent.ThreadFactory;
40 import java.util.concurrent.TimeUnit;
41 
42 public class TracingControllerTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
43 
44     public static class TracingReceiver extends OutputStream {
45         private int mChunkCount;
46         private boolean mComplete;
47         private ByteArrayOutputStream outputStream;
48 
TracingReceiver()49         public TracingReceiver() {
50             outputStream = new ByteArrayOutputStream();
51         }
52 
53         @Override
write(byte[] chunk)54         public void write(byte[] chunk) {
55             validateThread();
56             mChunkCount++;
57             try {
58                 outputStream.write(chunk);
59             } catch (IOException e) {
60                 throw new RuntimeException(e);
61             }
62         }
63 
64         @Override
close()65         public void close() {
66             validateThread();
67             mComplete = true;
68         }
69 
70         @Override
flush()71         public void flush() {
72             fail("flush should not be called");
73         }
74 
75         @Override
write(int b)76         public void write(int b) {
77             fail("write(int) should not be called");
78         }
79 
80         @Override
write(byte[] b, int off, int len)81         public void write(byte[] b, int off, int len) {
82             fail("write(byte[], int, int) should not be called");
83         }
84 
validateThread()85         private void validateThread() {
86             assertTrue("Callbacks should be called on the correct (executor) thread",
87                     Thread.currentThread().getName().startsWith(EXECUTOR_THREAD_PREFIX));
88         }
89 
getNbChunks()90         int getNbChunks() { return mChunkCount; }
getComplete()91         boolean getComplete() { return mComplete; }
92 
getCompleteCallable()93         Callable<Boolean> getCompleteCallable() {
94             return new Callable<Boolean>() {
95                 @Override
96                 public Boolean call() {
97                     return getComplete();
98                 }
99             };
100         }
101 
getOutputStream()102         ByteArrayOutputStream getOutputStream() { return outputStream; }
103     }
104 
105     private static final int POLLING_TIMEOUT = 60 * 1000;
106     private static final int EXECUTOR_TIMEOUT = 10; // timeout of executor shutdown in seconds
107     private static final String EXECUTOR_THREAD_PREFIX = "TracingExecutorThread";
108     private WebViewOnUiThread mOnUiThread;
109     private ExecutorService singleThreadExecutor;
110 
111     public TracingControllerTest() throws Exception {
112         super("android.webkit.cts", WebViewCtsActivity.class);
113     }
114 
115     @Override
116     protected void setUp() throws Exception {
117         super.setUp();
118         WebView webview = getActivity().getWebView();
119         if (webview == null) return;
120         mOnUiThread = new WebViewOnUiThread(webview);
121         singleThreadExecutor = Executors.newSingleThreadExecutor(getCustomThreadFactory());
122     }
123 
124     @Override
125     protected void tearDown() throws Exception {
126         // make sure to stop everything and clean up
127         ensureTracingStopped();
128         if (singleThreadExecutor != null) {
129             singleThreadExecutor.shutdown();
130             if (!singleThreadExecutor.awaitTermination(EXECUTOR_TIMEOUT, TimeUnit.SECONDS)) {
131                 fail("Failed to shutdown executor");
132             }
133         }
134         if (mOnUiThread != null) {
135             mOnUiThread.cleanUp();
136         }
137         super.tearDown();
138     }
139 
140     private void ensureTracingStopped() throws Exception {
141         if (!NullWebViewUtils.isWebViewAvailable()) {
142             return;
143         }
144 
145         TracingController.getInstance().stop(null, singleThreadExecutor);
146         Callable<Boolean> tracingStopped = new Callable<Boolean>() {
147             @Override
148             public Boolean call() {
149                 return !TracingController.getInstance().isTracing();
150             }
151         };
152         PollingCheck.check("Tracing did not stop", POLLING_TIMEOUT, tracingStopped);
153     }
154 
155     private ThreadFactory getCustomThreadFactory() {
156         return new ThreadFactory() {
157             private final AtomicInteger threadCount = new AtomicInteger(0);
158             @Override
159             public Thread newThread(Runnable r) {
160                 Thread thread = new Thread(r);
161                 thread.setName(EXECUTOR_THREAD_PREFIX + "_" + threadCount.incrementAndGet());
162                 return thread;
163             }
164         };
165     }
166 
167     // Test that callbacks are invoked and tracing data is returned on the correct thread
168     // (via executor). Tracing start/stop and webview loading happens on the UI thread.
169     public void testTracingControllerCallbacksOnUI() throws Throwable {
170         if (!NullWebViewUtils.isWebViewAvailable()) {
171             return;
172         }
173         final TracingReceiver tracingReceiver = new TracingReceiver();
174 
175         WebkitUtils.onMainThreadSync(() -> {
176             runTracingTestWithCallbacks(tracingReceiver, singleThreadExecutor);
177         });
178         PollingCheck.check("Tracing did not complete", POLLING_TIMEOUT, tracingReceiver.getCompleteCallable());
179         assertThat(tracingReceiver.getNbChunks(), greaterThan(0));
180         assertThat(tracingReceiver.getOutputStream().size(), greaterThan(0));
181         // currently the output is json (as of April 2018), but this could change in the future
182         // so we don't explicitly test the contents of output stream.
183     }
184 
185     // Test that callbacks are invoked and tracing data is returned on the correct thread
186     // (via executor). Tracing start/stop happens on the testing thread; webview loading
187     // happens on the UI thread.
188     public void testTracingControllerCallbacks() throws Throwable {
189         if (!NullWebViewUtils.isWebViewAvailable()) {
190             return;
191         }
192 
193         final TracingReceiver tracingReceiver = new TracingReceiver();
194         runTracingTestWithCallbacks(tracingReceiver, singleThreadExecutor);
195         PollingCheck.check("Tracing did not complete", POLLING_TIMEOUT, tracingReceiver.getCompleteCallable());
196         assertThat(tracingReceiver.getNbChunks(), greaterThan(0));
197         assertThat(tracingReceiver.getOutputStream().size(), greaterThan(0));
198     }
199 
200     // Test that tracing stop has no effect if tracing has not been started.
201     public void testTracingStopFalseIfNotTracing() {
202         if (!NullWebViewUtils.isWebViewAvailable()) {
203             return;
204         }
205 
206         TracingController tracingController = TracingController.getInstance();
207         assertFalse(tracingController.stop(null, singleThreadExecutor));
208         assertFalse(tracingController.isTracing());
209     }
210 
211     // Test that tracing cannot be started if already tracing.
212     public void testTracingCannotStartIfAlreadyTracing() throws Exception {
213         if (!NullWebViewUtils.isWebViewAvailable()) {
214             return;
215         }
216 
217         TracingController tracingController = TracingController.getInstance();
218         TracingConfig config = new TracingConfig.Builder().build();
219 
220         tracingController.start(config);
221         assertTrue(tracingController.isTracing());
222         try {
223             tracingController.start(config);
224         } catch (IllegalStateException e) {
225             // as expected
226             return;
227         }
228         assertTrue(tracingController.stop(null, singleThreadExecutor));
229         fail("Tracing start should throw an exception when attempting to start while already tracing");
230     }
231 
232     // Test that tracing cannot be invoked with excluded categories.
233     public void testTracingInvalidCategoriesPatternExclusion() {
234         if (!NullWebViewUtils.isWebViewAvailable()) {
235             return;
236         }
237 
238         TracingController tracingController = TracingController.getInstance();
239         TracingConfig config = new TracingConfig.Builder()
240                 .addCategories("android_webview","-blink")
241                 .build();
242         try {
243             tracingController.start(config);
244         } catch (IllegalArgumentException e) {
245             // as expected;
246             assertFalse("TracingController should not be tracing", tracingController.isTracing());
247             return;
248         }
249 
250         fail("Tracing start should throw an exception due to invalid category pattern");
251     }
252 
253     // Test that tracing cannot be invoked with categories containing commas.
254     public void testTracingInvalidCategoriesPatternComma() {
255         if (!NullWebViewUtils.isWebViewAvailable()) {
256             return;
257         }
258 
259         TracingController tracingController = TracingController.getInstance();
260         TracingConfig config = new TracingConfig.Builder()
261                 .addCategories("android_webview, blink")
262                 .build();
263         try {
264             tracingController.start(config);
265         } catch (IllegalArgumentException e) {
266             // as expected;
267             assertFalse("TracingController should not be tracing", tracingController.isTracing());
268             return;
269         }
270 
271         fail("Tracing start should throw an exception due to invalid category pattern");
272     }
273 
274     // Test that tracing cannot start with a configuration that is null.
275     public void testTracingWithNullConfig() {
276         if (!NullWebViewUtils.isWebViewAvailable()) {
277             return;
278         }
279 
280         TracingController tracingController = TracingController.getInstance();
281         try {
282             tracingController.start(null);
283         } catch (IllegalArgumentException e) {
284             // as expected
285             assertFalse("TracingController should not be tracing", tracingController.isTracing());
286             return;
287         }
288         fail("Tracing start should throw exception if TracingConfig is null");
289     }
290 
291     // Generic helper function for running tracing.
292     private void runTracingTestWithCallbacks(TracingReceiver tracingReceiver, Executor executor) {
293         TracingController tracingController = TracingController.getInstance();
294         assertNotNull(tracingController);
295 
296         TracingConfig config = new TracingConfig.Builder()
297                 .addCategories(TracingConfig.CATEGORIES_WEB_DEVELOPER)
298                 .setTracingMode(TracingConfig.RECORD_CONTINUOUSLY)
299                 .build();
300         assertFalse(tracingController.isTracing());
301         tracingController.start(config);
302         assertTrue(tracingController.isTracing());
303 
304         mOnUiThread.loadUrlAndWaitForCompletion("about:blank");
305         assertTrue(tracingController.stop(tracingReceiver, executor));
306     }
307 }
308 
309