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