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