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