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