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