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