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