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}