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