LayoutTestsExecutor.java revision f0f30c677a187b0436e62d2be6c97b76e61d74ea
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 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 + "::startTests", e.getMessage());
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        mCurrentTestTimedOut = true;
373
374        /**
375         * While it is theoretically possible that the test times out because
376         * of webview becoming unresponsive, it is very unlikely. Therefore it's
377         * assumed that obtaining results (that calls various webview methods)
378         * will not itself hang.
379         */
380        obtainActualResultsFromWebView();
381    }
382
383    private void onTestFinished() {
384        assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name();
385
386        obtainActualResultsFromWebView();
387    }
388
389    private void obtainActualResultsFromWebView() {
390        /**
391         * If the result has not been set by the time the test finishes we create
392         * a default type of result.
393         */
394        if (mCurrentResult == null) {
395            /** TODO: Default type should be RenderTreeResult. We don't support it now. */
396            mCurrentResult = new TextResult(mCurrentTestRelativePath);
397        }
398
399        mCurrentState = CurrentState.OBTAINING_RESULT;
400
401        mCurrentResult.obtainActualResults(mCurrentWebView,
402                mResultHandler.obtainMessage(MSG_ACTUAL_RESULT_OBTAINED));
403    }
404
405    private void onActualResultsObtained() {
406        assert mCurrentState == CurrentState.OBTAINING_RESULT
407                : "mCurrentState = " + mCurrentState.name();
408
409        mCurrentState = CurrentState.IDLE;
410
411        mResultHandler.removeMessages(MSG_TEST_TIMED_OUT);
412        reportResultToService();
413        mCurrentTestIndex++;
414        updateProgressBar();
415        runNextTest();
416    }
417
418    private void reportResultToService() {
419        if (mCurrentAdditionalTextOutput != null) {
420            mCurrentResult.setAdditionalTextOutputString(mCurrentAdditionalTextOutput.toString());
421        }
422
423        try {
424            Message serviceMsg =
425                    Message.obtain(null, ManagerService.MSG_PROCESS_ACTUAL_RESULTS);
426
427            Bundle bundle = mCurrentResult.getBundle();
428            bundle.putInt("testIndex", mCurrentTestIndex);
429            if (mCurrentTestTimedOut) {
430                bundle.putString("resultCode", AbstractResult.ResultCode.FAIL_TIMED_OUT.name());
431            }
432            if (!mTestsList.isEmpty()) {
433                bundle.putString("nextTest", mTestsList.get(0));
434            }
435
436            serviceMsg.setData(bundle);
437            mManagerServiceMessenger.send(serviceMsg);
438        } catch (RemoteException e) {
439            Log.e(LOG_TAG + "::reportResultToService", e.getMessage());
440        }
441    }
442
443    private void updateProgressBar() {
444        getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
445                mCurrentTestIndex * Window.PROGRESS_END / mTotalTestCount);
446        setTitle(mCurrentTestIndex * 100 / mTotalTestCount + "% " +
447                "(" + mCurrentTestIndex + "/" + mTotalTestCount + ")");
448    }
449
450    private void onAllTestsFinished() {
451        mScreenDimLock.release();
452
453        try {
454            Message serviceMsg =
455                    Message.obtain(null, ManagerService.MSG_ALL_TESTS_FINISHED);
456            mManagerServiceMessenger.send(serviceMsg);
457        } catch (RemoteException e) {
458            Log.e(LOG_TAG + "::onAllTestsFinished", e.getMessage());
459        }
460
461        unbindService(mServiceConnection);
462    }
463
464    private AdditionalTextOutput getCurrentAdditionalTextOutput() {
465        if (mCurrentAdditionalTextOutput == null) {
466            mCurrentAdditionalTextOutput = new AdditionalTextOutput();
467        }
468        return mCurrentAdditionalTextOutput;
469    }
470
471    /** LAYOUT TEST CONTROLLER */
472
473    private static final int MSG_WAIT_UNTIL_DONE = 0;
474    private static final int MSG_NOTIFY_DONE = 1;
475    private static final int MSG_DUMP_AS_TEXT = 2;
476    private static final int MSG_DUMP_CHILD_FRAMES_AS_TEXT = 3;
477    private static final int MSG_SET_CAN_OPEN_WINDOWS = 4;
478    private static final int MSG_DUMP_DATABASE_CALLBACKS = 5;
479    private static final int MSG_SET_GEOLOCATION_PERMISSION = 6;
480
481    Handler mLayoutTestControllerHandler = new Handler() {
482        @Override
483        public void handleMessage(Message msg) {
484            assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name();
485
486            switch (msg.what) {
487                case MSG_WAIT_UNTIL_DONE:
488                    mCurrentState = CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST;
489                    break;
490
491                case MSG_NOTIFY_DONE:
492                    if (mCurrentState == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST) {
493                        onTestFinished();
494                    }
495                    break;
496
497                case MSG_DUMP_AS_TEXT:
498                    if (mCurrentResult == null) {
499                        mCurrentResult = new TextResult(mCurrentTestRelativePath);
500                    }
501
502                    assert mCurrentResult instanceof TextResult
503                            : "mCurrentResult instanceof" + mCurrentResult.getClass().getName();
504
505                    break;
506
507                case MSG_DUMP_CHILD_FRAMES_AS_TEXT:
508                    /** If dumpAsText was not called we assume that the result should be text */
509                    if (mCurrentResult == null) {
510                        mCurrentResult = new TextResult(mCurrentTestRelativePath);
511                    }
512
513                    assert mCurrentResult instanceof TextResult
514                            : "mCurrentResult instanceof" + mCurrentResult.getClass().getName();
515
516                    ((TextResult)mCurrentResult).setDumpChildFramesAsText(true);
517                    break;
518
519                case MSG_SET_CAN_OPEN_WINDOWS:
520                    mCanOpenWindows = true;
521                    break;
522
523                case MSG_DUMP_DATABASE_CALLBACKS:
524                    mDumpDatabaseCallbacks = true;
525                    break;
526
527                case MSG_SET_GEOLOCATION_PERMISSION:
528                    mIsGeolocationPermissionSet = true;
529                    mGeolocationPermission = msg.arg1 == 1;
530
531                    if (mPendingGeolocationPermissionCallbacks != null) {
532                        Iterator iter = mPendingGeolocationPermissionCallbacks.keySet().iterator();
533                        while (iter.hasNext()) {
534                            GeolocationPermissions.Callback callback =
535                                    (GeolocationPermissions.Callback) iter.next();
536                            String origin = (String) mPendingGeolocationPermissionCallbacks.get(callback);
537                            callback.invoke(origin, mGeolocationPermission, false);
538                        }
539                        mPendingGeolocationPermissionCallbacks = null;
540                    }
541                    break;
542
543                default:
544                    Log.w(LOG_TAG + "::handleMessage", "Message code does not exist: " + msg.what);
545                    break;
546            }
547        }
548    };
549
550    private void resetLayoutTestController() {
551        mCanOpenWindows = false;
552        mDumpDatabaseCallbacks = false;
553        mIsGeolocationPermissionSet = false;
554        mPendingGeolocationPermissionCallbacks = null;
555    }
556
557    public void waitUntilDone() {
558        Log.w(LOG_TAG + "::waitUntilDone", "called");
559        mLayoutTestControllerHandler.sendEmptyMessage(MSG_WAIT_UNTIL_DONE);
560    }
561
562    public void notifyDone() {
563        Log.w(LOG_TAG + "::notifyDone", "called");
564        mLayoutTestControllerHandler.sendEmptyMessage(MSG_NOTIFY_DONE);
565    }
566
567    public void dumpAsText(boolean enablePixelTest) {
568        Log.w(LOG_TAG + "::dumpAsText(" + enablePixelTest + ")", "called");
569        /** TODO: Implement */
570        if (enablePixelTest) {
571            Log.w(LOG_TAG + "::dumpAsText", "enablePixelTest not implemented, switching to false");
572        }
573        mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_AS_TEXT);
574    }
575
576    public void dumpChildFramesAsText() {
577        Log.w(LOG_TAG + "::dumpChildFramesAsText", "called");
578        mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_CHILD_FRAMES_AS_TEXT);
579    }
580
581    public void setCanOpenWindows() {
582        Log.w(LOG_TAG + "::setCanOpenWindows", "called");
583        mLayoutTestControllerHandler.sendEmptyMessage(MSG_SET_CAN_OPEN_WINDOWS);
584    }
585
586    public void dumpDatabaseCallbacks() {
587        Log.w(LOG_TAG + "::dumpDatabaseCallbacks:", "called");
588        mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_DATABASE_CALLBACKS);
589    }
590
591    public void setGeolocationPermission(boolean allow) {
592        Log.w(LOG_TAG + "::setGeolocationPermission", "called");
593        Message msg = mLayoutTestControllerHandler.obtainMessage(MSG_SET_GEOLOCATION_PERMISSION);
594        msg.arg1 = allow ? 1 : 0;
595        msg.sendToTarget();
596    }
597
598    public void setMockDeviceOrientation(boolean canProvideAlpha, double alpha,
599            boolean canProvideBeta, double beta, boolean canProvideGamma, double gamma) {
600        mCurrentWebView.setMockDeviceOrientation(canProvideAlpha, alpha, canProvideBeta, beta,
601                canProvideGamma, gamma);
602    }
603}
604