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