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