LayoutTestsExecutor.java revision c6a341d34c690c2b5948977cd803ec2ff668c4b7
1/* 2 * Copyright (C) 2010 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 17package com.android.dumprendertree2; 18 19import android.app.Activity; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.ServiceConnection; 24import android.net.Uri; 25import android.os.Bundle; 26import android.os.Environment; 27import android.os.Handler; 28import android.os.IBinder; 29import android.os.Message; 30import android.os.Messenger; 31import android.os.PowerManager; 32import android.os.RemoteException; 33import android.os.PowerManager.WakeLock; 34import android.util.Log; 35import android.view.Window; 36import android.webkit.ConsoleMessage; 37import android.webkit.JsPromptResult; 38import android.webkit.JsResult; 39import android.webkit.WebChromeClient; 40import android.webkit.WebSettings; 41import android.webkit.WebView; 42import android.webkit.WebViewClient; 43import android.webkit.GeolocationPermissions; 44import android.webkit.WebStorage.QuotaUpdater; 45 46import java.io.File; 47import java.net.MalformedURLException; 48import java.net.URL; 49import java.util.HashMap; 50import java.util.Iterator; 51import java.util.List; 52import java.util.Map; 53 54/** 55 * This activity executes the test. It contains WebView and logic of LayoutTestController 56 * functions. It runs in a separate process and sends the results of running the test 57 * to ManagerService. The reason why is to handle crashing (test that crashes brings down 58 * whole process with it). 59 */ 60public class LayoutTestsExecutor extends Activity { 61 62 private enum CurrentState { 63 IDLE, 64 RENDERING_PAGE, 65 WAITING_FOR_ASYNCHRONOUS_TEST, 66 OBTAINING_RESULT; 67 68 public boolean isRunningState() { 69 return this == CurrentState.RENDERING_PAGE || 70 this == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST; 71 } 72 } 73 74 /** TODO: make it a setting */ 75 static final String TESTS_ROOT_DIR_PATH = 76 Environment.getExternalStorageDirectory() + 77 File.separator + "android" + 78 File.separator + "LayoutTests"; 79 80 private static final String LOG_TAG = "LayoutTestExecutor"; 81 82 public static final String EXTRA_TESTS_LIST = "TestsList"; 83 public static final String EXTRA_TEST_INDEX = "TestIndex"; 84 85 private static final int MSG_ACTUAL_RESULT_OBTAINED = 0; 86 private static final int MSG_TEST_TIMED_OUT = 1; 87 88 private static final int DEFAULT_TIME_OUT_MS = 15 * 1000; 89 90 /** A list of tests that remain to run since last crash */ 91 private List<String> mTestsList; 92 93 /** 94 * This is a number of currently running test. It is 0-based and doesn't reset after 95 * the crash. Initial index is passed to LayoutTestsExecuter in the intent that starts 96 * it. 97 */ 98 private int mCurrentTestIndex; 99 100 /** The total number of tests to run, doesn't reset after crash */ 101 private int mTotalTestCount; 102 103 private WebView mCurrentWebView; 104 private String mCurrentTestRelativePath; 105 private String mCurrentTestUri; 106 private CurrentState mCurrentState = CurrentState.IDLE; 107 108 private boolean mCurrentTestTimedOut; 109 private AbstractResult mCurrentResult; 110 private AdditionalTextOutput mCurrentAdditionalTextOutput; 111 112 private LayoutTestController mLayoutTestController = new LayoutTestController(this); 113 private boolean mCanOpenWindows; 114 private boolean mDumpDatabaseCallbacks; 115 private boolean mIsGeolocationPermissionSet; 116 private boolean mGeolocationPermission; 117 private Map<GeolocationPermissions.Callback, String> mPendingGeolocationPermissionCallbacks; 118 119 private EventSender mEventSender = new EventSender(); 120 121 private WakeLock mScreenDimLock; 122 123 /** COMMUNICATION WITH ManagerService */ 124 125 private Messenger mManagerServiceMessenger; 126 127 private ServiceConnection mServiceConnection = new ServiceConnection() { 128 129 @Override 130 public void onServiceConnected(ComponentName name, IBinder service) { 131 mManagerServiceMessenger = new Messenger(service); 132 startTests(); 133 } 134 135 @Override 136 public void onServiceDisconnected(ComponentName name) { 137 /** TODO */ 138 } 139 }; 140 141 private final Handler mResultHandler = new Handler() { 142 @Override 143 public void handleMessage(Message msg) { 144 switch (msg.what) { 145 case MSG_ACTUAL_RESULT_OBTAINED: 146 onActualResultsObtained(); 147 break; 148 149 case MSG_TEST_TIMED_OUT: 150 onTestTimedOut(); 151 break; 152 153 default: 154 break; 155 } 156 } 157 }; 158 159 /** WEBVIEW CONFIGURATION */ 160 161 private WebViewClient mWebViewClient = new WebViewClient() { 162 @Override 163 public void onPageFinished(WebView view, String url) { 164 /** Some tests fire up many page loads, we don't want to detect them */ 165 if (!url.equals(mCurrentTestUri)) { 166 return; 167 } 168 169 if (mCurrentState == CurrentState.RENDERING_PAGE) { 170 onTestFinished(); 171 } 172 } 173 }; 174 175 private WebChromeClient mWebChromeClient = new WebChromeClient() { 176 @Override 177 public void onExceededDatabaseQuota(String url, String databaseIdentifier, 178 long currentQuota, long estimatedSize, long totalUsedQuota, 179 QuotaUpdater quotaUpdater) { 180 /** TODO: This should be recorded as part of the text result */ 181 /** TODO: The quota should also probably be reset somehow for every test? */ 182 if (mDumpDatabaseCallbacks) { 183 getCurrentAdditionalTextOutput().appendExceededDbQuotaMessage(url, 184 databaseIdentifier); 185 } 186 quotaUpdater.updateQuota(currentQuota + 5 * 1024 * 1024); 187 } 188 189 @Override 190 public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 191 getCurrentAdditionalTextOutput().appendJsAlert(message); 192 result.confirm(); 193 return true; 194 } 195 196 @Override 197 public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 198 getCurrentAdditionalTextOutput().appendJsConfirm(message); 199 result.confirm(); 200 return true; 201 } 202 203 @Override 204 public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, 205 JsPromptResult result) { 206 getCurrentAdditionalTextOutput().appendJsPrompt(message, defaultValue); 207 result.confirm(); 208 return true; 209 } 210 211 @Override 212 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 213 getCurrentAdditionalTextOutput().appendConsoleMessage(consoleMessage); 214 return true; 215 } 216 217 @Override 218 public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, 219 Message resultMsg) { 220 WebView.WebViewTransport transport = (WebView.WebViewTransport)resultMsg.obj; 221 /** By default windows cannot be opened, so just send null back. */ 222 WebView newWindowWebView = null; 223 224 if (mCanOpenWindows) { 225 /** 226 * We never display the new window, just create the view and allow it's content to 227 * execute and be recorded by the executor. 228 */ 229 newWindowWebView = new WebView(LayoutTestsExecutor.this); 230 setupWebView(newWindowWebView); 231 } 232 233 transport.setWebView(newWindowWebView); 234 resultMsg.sendToTarget(); 235 return true; 236 } 237 238 @Override 239 public void onGeolocationPermissionsShowPrompt(String origin, 240 GeolocationPermissions.Callback callback) { 241 if (mIsGeolocationPermissionSet) { 242 callback.invoke(origin, mGeolocationPermission, false); 243 return; 244 } 245 if (mPendingGeolocationPermissionCallbacks == null) { 246 mPendingGeolocationPermissionCallbacks = 247 new HashMap<GeolocationPermissions.Callback, String>(); 248 } 249 mPendingGeolocationPermissionCallbacks.put(callback, origin); 250 } 251 }; 252 253 /** IMPLEMENTATION */ 254 255 @Override 256 protected void onCreate(Bundle savedInstanceState) { 257 super.onCreate(savedInstanceState); 258 259 requestWindowFeature(Window.FEATURE_PROGRESS); 260 261 Intent intent = getIntent(); 262 mTestsList = intent.getStringArrayListExtra(EXTRA_TESTS_LIST); 263 mCurrentTestIndex = intent.getIntExtra(EXTRA_TEST_INDEX, -1); 264 mTotalTestCount = mCurrentTestIndex + mTestsList.size(); 265 266 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 267 mScreenDimLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK 268 | PowerManager.ON_AFTER_RELEASE, "WakeLock in LayoutTester"); 269 mScreenDimLock.acquire(); 270 271 bindService(new Intent(this, ManagerService.class), mServiceConnection, 272 Context.BIND_AUTO_CREATE); 273 } 274 275 private void reset() { 276 WebView previousWebView = mCurrentWebView; 277 278 resetLayoutTestController(); 279 280 mCurrentTestTimedOut = false; 281 mCurrentResult = null; 282 mCurrentAdditionalTextOutput = null; 283 284 mCurrentWebView = new WebView(this); 285 setupWebView(mCurrentWebView); 286 287 mEventSender.reset(mCurrentWebView); 288 289 setContentView(mCurrentWebView); 290 if (previousWebView != null) { 291 Log.d(LOG_TAG + "::reset", "previousWebView != null"); 292 previousWebView.destroy(); 293 } 294 } 295 296 private void setupWebView(WebView webView) { 297 webView.setWebViewClient(mWebViewClient); 298 webView.setWebChromeClient(mWebChromeClient); 299 webView.addJavascriptInterface(mLayoutTestController, "layoutTestController"); 300 webView.addJavascriptInterface(mEventSender, "eventSender"); 301 302 /** 303 * Setting a touch interval of -1 effectively disables the optimisation in WebView 304 * that stops repeated touch events flooding WebCore. The Event Sender only sends a 305 * single event rather than a stream of events (like what would generally happen in 306 * a real use of touch events in a WebView) and so if the WebView drops the event, 307 * the test will fail as the test expects one callback for every touch it synthesizes. 308 */ 309 webView.setTouchInterval(-1); 310 311 webView.clearCache(true); 312 313 WebSettings webViewSettings = webView.getSettings(); 314 webViewSettings.setAppCacheEnabled(true); 315 webViewSettings.setAppCachePath(getApplicationContext().getCacheDir().getPath()); 316 webViewSettings.setAppCacheMaxSize(Long.MAX_VALUE); 317 webViewSettings.setJavaScriptEnabled(true); 318 webViewSettings.setJavaScriptCanOpenWindowsAutomatically(true); 319 webViewSettings.setSupportMultipleWindows(true); 320 webViewSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); 321 webViewSettings.setDatabaseEnabled(true); 322 webViewSettings.setDatabasePath(getDir("databases", 0).getAbsolutePath()); 323 webViewSettings.setDomStorageEnabled(true); 324 webViewSettings.setWorkersEnabled(false); 325 webViewSettings.setXSSAuditorEnabled(false); 326 327 // This is asynchronous, but it gets processed by WebCore before it starts loading pages. 328 mCurrentWebView.useMockDeviceOrientation(); 329 } 330 331 private void startTests() { 332 try { 333 Message serviceMsg = 334 Message.obtain(null, ManagerService.MSG_FIRST_TEST); 335 336 Bundle bundle = new Bundle(); 337 if (!mTestsList.isEmpty()) { 338 bundle.putString("firstTest", mTestsList.get(0)); 339 bundle.putInt("index", mCurrentTestIndex); 340 } 341 342 serviceMsg.setData(bundle); 343 mManagerServiceMessenger.send(serviceMsg); 344 } catch (RemoteException e) { 345 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e); 346 } 347 348 runNextTest(); 349 } 350 351 private void runNextTest() { 352 assert mCurrentState == CurrentState.IDLE : "mCurrentState = " + mCurrentState.name(); 353 354 if (mTestsList.isEmpty()) { 355 onAllTestsFinished(); 356 return; 357 } 358 359 mCurrentTestRelativePath = mTestsList.remove(0); 360 361 Log.i(LOG_TAG, "runNextTest(): Start: " + mCurrentTestRelativePath + 362 " (" + mCurrentTestIndex + ")"); 363 364 mCurrentTestUri = FileFilter.getUrl(mCurrentTestRelativePath).toString(); 365 366 reset(); 367 368 /** Start time-out countdown and the test */ 369 mCurrentState = CurrentState.RENDERING_PAGE; 370 mResultHandler.sendEmptyMessageDelayed(MSG_TEST_TIMED_OUT, DEFAULT_TIME_OUT_MS); 371 mCurrentWebView.loadUrl(mCurrentTestUri); 372 } 373 374 private void onTestTimedOut() { 375 assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); 376 377 Log.w(LOG_TAG, "onTestTimedOut(): " + mCurrentTestRelativePath); 378 mCurrentTestTimedOut = true; 379 380 /** 381 * While it is theoretically possible that the test times out because 382 * of webview becoming unresponsive, it is very unlikely. Therefore it's 383 * assumed that obtaining results (that calls various webview methods) 384 * will not itself hang. 385 */ 386 obtainActualResultsFromWebView(); 387 } 388 389 private void onTestFinished() { 390 assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); 391 392 Log.i(LOG_TAG, "onTestFinished(): " + mCurrentTestRelativePath); 393 obtainActualResultsFromWebView(); 394 } 395 396 private void obtainActualResultsFromWebView() { 397 /** 398 * If the result has not been set by the time the test finishes we create 399 * a default type of result. 400 */ 401 if (mCurrentResult == null) { 402 /** TODO: Default type should be RenderTreeResult. We don't support it now. */ 403 mCurrentResult = new TextResult(mCurrentTestRelativePath); 404 } 405 406 mCurrentState = CurrentState.OBTAINING_RESULT; 407 408 mCurrentResult.obtainActualResults(mCurrentWebView, 409 mResultHandler.obtainMessage(MSG_ACTUAL_RESULT_OBTAINED)); 410 } 411 412 private void onActualResultsObtained() { 413 assert mCurrentState == CurrentState.OBTAINING_RESULT 414 : "mCurrentState = " + mCurrentState.name(); 415 416 Log.i(LOG_TAG, "onActualResultsObtained(): " + mCurrentTestRelativePath); 417 mCurrentState = CurrentState.IDLE; 418 419 mResultHandler.removeMessages(MSG_TEST_TIMED_OUT); 420 reportResultToService(); 421 mCurrentTestIndex++; 422 updateProgressBar(); 423 runNextTest(); 424 } 425 426 private void reportResultToService() { 427 if (mCurrentAdditionalTextOutput != null) { 428 mCurrentResult.setAdditionalTextOutputString(mCurrentAdditionalTextOutput.toString()); 429 } 430 431 try { 432 Message serviceMsg = 433 Message.obtain(null, ManagerService.MSG_PROCESS_ACTUAL_RESULTS); 434 435 Bundle bundle = mCurrentResult.getBundle(); 436 bundle.putInt("testIndex", mCurrentTestIndex); 437 if (mCurrentTestTimedOut) { 438 bundle.putString("resultCode", AbstractResult.ResultCode.FAIL_TIMED_OUT.name()); 439 } 440 if (!mTestsList.isEmpty()) { 441 bundle.putString("nextTest", mTestsList.get(0)); 442 } 443 444 serviceMsg.setData(bundle); 445 mManagerServiceMessenger.send(serviceMsg); 446 } catch (RemoteException e) { 447 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e); 448 } 449 } 450 451 private void updateProgressBar() { 452 getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 453 mCurrentTestIndex * Window.PROGRESS_END / mTotalTestCount); 454 setTitle(mCurrentTestIndex * 100 / mTotalTestCount + "% " + 455 "(" + mCurrentTestIndex + "/" + mTotalTestCount + ")"); 456 } 457 458 private void onAllTestsFinished() { 459 mScreenDimLock.release(); 460 461 try { 462 Message serviceMsg = 463 Message.obtain(null, ManagerService.MSG_ALL_TESTS_FINISHED); 464 mManagerServiceMessenger.send(serviceMsg); 465 } catch (RemoteException e) { 466 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e); 467 } 468 469 unbindService(mServiceConnection); 470 } 471 472 private AdditionalTextOutput getCurrentAdditionalTextOutput() { 473 if (mCurrentAdditionalTextOutput == null) { 474 mCurrentAdditionalTextOutput = new AdditionalTextOutput(); 475 } 476 return mCurrentAdditionalTextOutput; 477 } 478 479 /** LAYOUT TEST CONTROLLER */ 480 481 private static final int MSG_WAIT_UNTIL_DONE = 0; 482 private static final int MSG_NOTIFY_DONE = 1; 483 private static final int MSG_DUMP_AS_TEXT = 2; 484 private static final int MSG_DUMP_CHILD_FRAMES_AS_TEXT = 3; 485 private static final int MSG_SET_CAN_OPEN_WINDOWS = 4; 486 private static final int MSG_DUMP_DATABASE_CALLBACKS = 5; 487 private static final int MSG_SET_GEOLOCATION_PERMISSION = 6; 488 489 Handler mLayoutTestControllerHandler = new Handler() { 490 @Override 491 public void handleMessage(Message msg) { 492 assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); 493 494 switch (msg.what) { 495 case MSG_WAIT_UNTIL_DONE: 496 mCurrentState = CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST; 497 break; 498 499 case MSG_NOTIFY_DONE: 500 if (mCurrentState == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST) { 501 onTestFinished(); 502 } 503 break; 504 505 case MSG_DUMP_AS_TEXT: 506 if (mCurrentResult == null) { 507 mCurrentResult = new TextResult(mCurrentTestRelativePath); 508 } 509 510 assert mCurrentResult instanceof TextResult 511 : "mCurrentResult instanceof" + mCurrentResult.getClass().getName(); 512 513 break; 514 515 case MSG_DUMP_CHILD_FRAMES_AS_TEXT: 516 /** If dumpAsText was not called we assume that the result should be text */ 517 if (mCurrentResult == null) { 518 mCurrentResult = new TextResult(mCurrentTestRelativePath); 519 } 520 521 assert mCurrentResult instanceof TextResult 522 : "mCurrentResult instanceof" + mCurrentResult.getClass().getName(); 523 524 ((TextResult)mCurrentResult).setDumpChildFramesAsText(true); 525 break; 526 527 case MSG_SET_CAN_OPEN_WINDOWS: 528 mCanOpenWindows = true; 529 break; 530 531 case MSG_DUMP_DATABASE_CALLBACKS: 532 mDumpDatabaseCallbacks = true; 533 break; 534 535 case MSG_SET_GEOLOCATION_PERMISSION: 536 mIsGeolocationPermissionSet = true; 537 mGeolocationPermission = msg.arg1 == 1; 538 539 if (mPendingGeolocationPermissionCallbacks != null) { 540 Iterator<GeolocationPermissions.Callback> iter = 541 mPendingGeolocationPermissionCallbacks.keySet().iterator(); 542 while (iter.hasNext()) { 543 GeolocationPermissions.Callback callback = iter.next(); 544 String origin = mPendingGeolocationPermissionCallbacks.get(callback); 545 callback.invoke(origin, mGeolocationPermission, false); 546 } 547 mPendingGeolocationPermissionCallbacks = null; 548 } 549 break; 550 551 default: 552 assert false : "msg.what=" + msg.what; 553 break; 554 } 555 } 556 }; 557 558 private void resetLayoutTestController() { 559 mCanOpenWindows = false; 560 mDumpDatabaseCallbacks = false; 561 mIsGeolocationPermissionSet = false; 562 mPendingGeolocationPermissionCallbacks = null; 563 } 564 565 public void waitUntilDone() { 566 Log.i(LOG_TAG, mCurrentTestRelativePath + ": waitUntilDone() called"); 567 mLayoutTestControllerHandler.sendEmptyMessage(MSG_WAIT_UNTIL_DONE); 568 } 569 570 public void notifyDone() { 571 Log.i(LOG_TAG, mCurrentTestRelativePath + ": notifyDone() called"); 572 mLayoutTestControllerHandler.sendEmptyMessage(MSG_NOTIFY_DONE); 573 } 574 575 public void dumpAsText(boolean enablePixelTest) { 576 Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpAsText(" + enablePixelTest + ") called"); 577 /** TODO: Implement */ 578 if (enablePixelTest) { 579 Log.w(LOG_TAG, "enablePixelTest not implemented, switching to false"); 580 } 581 mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_AS_TEXT); 582 } 583 584 public void dumpChildFramesAsText() { 585 Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpChildFramesAsText() called"); 586 mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_CHILD_FRAMES_AS_TEXT); 587 } 588 589 public void setCanOpenWindows() { 590 Log.i(LOG_TAG, mCurrentTestRelativePath + ": setCanOpenWindows() called"); 591 mLayoutTestControllerHandler.sendEmptyMessage(MSG_SET_CAN_OPEN_WINDOWS); 592 } 593 594 public void dumpDatabaseCallbacks() { 595 Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpDatabaseCallbacks() called"); 596 mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_DATABASE_CALLBACKS); 597 } 598 599 public void setGeolocationPermission(boolean allow) { 600 Log.i(LOG_TAG, mCurrentTestRelativePath + ": setGeolocationPermission(" + allow + 601 ") called"); 602 Message msg = mLayoutTestControllerHandler.obtainMessage(MSG_SET_GEOLOCATION_PERMISSION); 603 msg.arg1 = allow ? 1 : 0; 604 msg.sendToTarget(); 605 } 606 607 public void setMockDeviceOrientation(boolean canProvideAlpha, double alpha, 608 boolean canProvideBeta, double beta, boolean canProvideGamma, double gamma) { 609 Log.i(LOG_TAG, mCurrentTestRelativePath + ": setMockDeviceOrientation(" + canProvideAlpha + 610 ", " + alpha + ", " + canProvideBeta + ", " + beta + ", " + canProvideGamma + 611 ", " + gamma + ")"); 612 mCurrentWebView.setMockDeviceOrientation(canProvideAlpha, alpha, canProvideBeta, beta, 613 canProvideGamma, gamma); 614 } 615} 616