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