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