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