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