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