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