1/*
2 * Copyright (C) 2008 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.dumprendertree;
18
19import com.android.dumprendertree.TestShellActivity.DumpDataType;
20import com.android.dumprendertree.forwarder.AdbUtils;
21import com.android.dumprendertree.forwarder.ForwardService;
22
23import android.content.Context;
24import android.content.Intent;
25import android.os.Environment;
26import android.test.ActivityInstrumentationTestCase2;
27import android.util.Log;
28
29import java.io.BufferedOutputStream;
30import java.io.BufferedReader;
31import java.io.File;
32import java.io.FileNotFoundException;
33import java.io.FileOutputStream;
34import java.io.FileReader;
35import java.io.IOException;
36import java.io.InputStream;
37import java.io.OutputStream;
38import java.util.Vector;
39
40// TestRecorder creates four files ...
41// - passing tests
42// - failing tests
43// - tests for which results are ignored
44// - tests with no text results available
45// TestRecorder does not have the ability to clear the results.
46class MyTestRecorder {
47    private BufferedOutputStream mBufferedOutputPassedStream;
48    private BufferedOutputStream mBufferedOutputFailedStream;
49    private BufferedOutputStream mBufferedOutputIgnoreResultStream;
50    private BufferedOutputStream mBufferedOutputNoResultStream;
51
52    public void passed(String layout_file) {
53        try {
54            mBufferedOutputPassedStream.write(layout_file.getBytes());
55            mBufferedOutputPassedStream.write('\n');
56            mBufferedOutputPassedStream.flush();
57        } catch(Exception e) {
58            e.printStackTrace();
59        }
60    }
61
62    public void failed(String layout_file) {
63        try {
64            mBufferedOutputFailedStream.write(layout_file.getBytes());
65            mBufferedOutputFailedStream.write('\n');
66            mBufferedOutputFailedStream.flush();
67        } catch(Exception e) {
68            e.printStackTrace();
69        }
70    }
71
72    public void ignoreResult(String layout_file) {
73        try {
74            mBufferedOutputIgnoreResultStream.write(layout_file.getBytes());
75            mBufferedOutputIgnoreResultStream.write('\n');
76            mBufferedOutputIgnoreResultStream.flush();
77        } catch(Exception e) {
78            e.printStackTrace();
79        }
80    }
81
82    public void noResult(String layout_file) {
83        try {
84            mBufferedOutputNoResultStream.write(layout_file.getBytes());
85            mBufferedOutputNoResultStream.write('\n');
86            mBufferedOutputNoResultStream.flush();
87        } catch(Exception e) {
88            e.printStackTrace();
89        }
90    }
91
92    public MyTestRecorder(boolean resume) {
93        try {
94            File externalDir = Environment.getExternalStorageDirectory();
95            File resultsPassedFile = new File(externalDir, "layout_tests_passed.txt");
96            File resultsFailedFile = new File(externalDir, "layout_tests_failed.txt");
97            File resultsIgnoreResultFile = new File(externalDir, "layout_tests_ignored.txt");
98            File noExpectedResultFile = new File(externalDir, "layout_tests_nontext.txt");
99
100            mBufferedOutputPassedStream =
101                new BufferedOutputStream(new FileOutputStream(resultsPassedFile, resume));
102            mBufferedOutputFailedStream =
103                new BufferedOutputStream(new FileOutputStream(resultsFailedFile, resume));
104            mBufferedOutputIgnoreResultStream =
105                new BufferedOutputStream(new FileOutputStream(resultsIgnoreResultFile, resume));
106            mBufferedOutputNoResultStream =
107                new BufferedOutputStream(new FileOutputStream(noExpectedResultFile, resume));
108        } catch (Exception e) {
109            e.printStackTrace();
110        }
111    }
112
113    public void close() {
114        try {
115            mBufferedOutputPassedStream.close();
116            mBufferedOutputFailedStream.close();
117            mBufferedOutputIgnoreResultStream.close();
118            mBufferedOutputNoResultStream.close();
119        } catch (Exception e) {
120            e.printStackTrace();
121        }
122    }
123}
124
125
126public class LayoutTestsAutoTest extends ActivityInstrumentationTestCase2<TestShellActivity> {
127
128    private static final String LOGTAG = "LayoutTests";
129    static final int DEFAULT_TIMEOUT_IN_MILLIS = 5000;
130
131    static final String EXTERNAL_DIR = Environment.getExternalStorageDirectory().toString();
132    static final String LAYOUT_TESTS_ROOT = EXTERNAL_DIR + "/webkit/layout_tests/";
133    static final String LAYOUT_TESTS_RESULT_DIR = EXTERNAL_DIR + "/webkit/layout_tests_results/";
134    static final String ANDROID_EXPECTED_RESULT_DIR = EXTERNAL_DIR + "/webkit/expected_results/";
135    static final String LAYOUT_TESTS_LIST_FILE = EXTERNAL_DIR + "/webkit/layout_tests_list.txt";
136    static final String TEST_STATUS_FILE = EXTERNAL_DIR + "/webkit/running_test.txt";
137    static final String LAYOUT_TESTS_RESULTS_REFERENCE_FILES[] = {
138          "results/layout_tests_passed.txt",
139          "results/layout_tests_failed.txt",
140          "results/layout_tests_nontext.txt",
141          "results/layout_tests_crashed.txt",
142          "run_layout_tests.py"
143    };
144
145    static final String LAYOUT_RESULTS_FAILED_RESULT_FILE = "results/layout_tests_failed.txt";
146    static final String LAYOUT_RESULTS_NONTEXT_RESULT_FILE = "results/layout_tests_nontext.txt";
147    static final String LAYOUT_RESULTS_CRASHED_RESULT_FILE = "results/layout_tests_crashed.txt";
148    static final String LAYOUT_TESTS_RUNNER = "run_layout_tests.py";
149
150    private MyTestRecorder mResultRecorder;
151    private Vector<String> mTestList;
152    // Whether we should ignore the result for the corresponding test. Ordered same as mTestList.
153    private Vector<Boolean> mTestListIgnoreResult;
154    private boolean mRebaselineResults;
155    // The JavaScript engine currently in use. This determines which set of Android-specific
156    // expected test results we use.
157    private String mJsEngine;
158    private String mTestPathPrefix;
159    private boolean mFinished;
160    private int mTestCount;
161    private int mResumeIndex;
162
163    public LayoutTestsAutoTest() {
164      super(TestShellActivity.class);
165    }
166
167    private void getTestList() {
168        // Read test list.
169        try {
170            BufferedReader inReader = new BufferedReader(new FileReader(LAYOUT_TESTS_LIST_FILE));
171            String line = inReader.readLine();
172            while (line != null) {
173                if (line.startsWith(mTestPathPrefix)) {
174                    String[] components = line.split(" ");
175                    mTestList.add(components[0]);
176                    mTestListIgnoreResult.add(components.length > 1 && components[1].equals("IGNORE_RESULT"));
177                }
178                line = inReader.readLine();
179            }
180            inReader.close();
181            Log.v(LOGTAG, "Test list has " + mTestList.size() + " test(s).");
182        } catch (Exception e) {
183            Log.e(LOGTAG, "Error while reading test list : " + e.getMessage());
184        }
185        mTestCount = mTestList.size();
186    }
187
188    private void resumeTestList() {
189        // read out the test name it stoped last time.
190        try {
191            String line = FsUtils.readTestStatus(TEST_STATUS_FILE);
192            for (int i = 0; i < mTestList.size(); i++) {
193                if (mTestList.elementAt(i).equals(line)) {
194                    mTestList = new Vector<String>(mTestList.subList(i+1, mTestList.size()));
195                    mTestListIgnoreResult = new Vector<Boolean>(mTestListIgnoreResult.subList(i+1, mTestListIgnoreResult.size()));
196                    mResumeIndex = i + 1;
197                    break;
198                }
199            }
200        } catch (Exception e) {
201            Log.e(LOGTAG, "Error reading " + TEST_STATUS_FILE);
202        }
203    }
204
205    private void clearTestStatus() {
206        // Delete TEST_STATUS_FILE
207        try {
208            File f = new File(TEST_STATUS_FILE);
209            if (f.delete())
210                Log.v(LOGTAG, "Deleted " + TEST_STATUS_FILE);
211            else
212                Log.e(LOGTAG, "Fail to delete " + TEST_STATUS_FILE);
213        } catch (Exception e) {
214            Log.e(LOGTAG, "Fail to delete " + TEST_STATUS_FILE + " : " + e.getMessage());
215        }
216    }
217
218    private String getResultFile(String test) {
219        String shortName = test.substring(0, test.lastIndexOf('.'));
220        // Write actual results to result directory.
221        return shortName.replaceFirst(LAYOUT_TESTS_ROOT, LAYOUT_TESTS_RESULT_DIR) + "-result.txt";
222    }
223
224    // Gets the file which contains WebKit's expected results for this test.
225    private String getExpectedResultFile(String test) {
226        // The generic result is at <path>/<name>-expected.txt
227        // First try the Android-specific result at
228        // platform/android-<js-engine>/<path>/<name>-expected.txt
229        // then
230        // platform/android/<path>/<name>-expected.txt
231        int pos = test.lastIndexOf('.');
232        if (pos == -1)
233            return null;
234        String genericExpectedResult = test.substring(0, pos) + "-expected.txt";
235        String androidExpectedResultsDir = "platform/android-" + mJsEngine + "/";
236        String androidExpectedResult = genericExpectedResult.replaceFirst(LAYOUT_TESTS_ROOT,
237                LAYOUT_TESTS_ROOT + androidExpectedResultsDir);
238        File f = new File(androidExpectedResult);
239        if (f.exists())
240            return androidExpectedResult;
241        androidExpectedResultsDir = "platform/android/";
242        androidExpectedResult = genericExpectedResult.replaceFirst(LAYOUT_TESTS_ROOT,
243                LAYOUT_TESTS_ROOT + androidExpectedResultsDir);
244        f = new File(androidExpectedResult);
245        return f.exists() ? androidExpectedResult : genericExpectedResult;
246    }
247
248    // Gets the file which contains the actual results of running the test on
249    // Android, generated by a previous run which set a new baseline.
250    private String getAndroidExpectedResultFile(String expectedResultFile) {
251        return expectedResultFile.replaceFirst(LAYOUT_TESTS_ROOT, ANDROID_EXPECTED_RESULT_DIR);
252    }
253
254    // Wrap up
255    private void failedCase(String file) {
256        Log.w("Layout test: ", file + " failed");
257        mResultRecorder.failed(file);
258    }
259
260    private void passedCase(String file) {
261        Log.v("Layout test:", file + " passed");
262        mResultRecorder.passed(file);
263    }
264
265    private void ignoreResultCase(String file) {
266        Log.v("Layout test:", file + " ignore result");
267        mResultRecorder.ignoreResult(file);
268    }
269
270    private void noResultCase(String file) {
271        Log.v("Layout test:", file + " no expected result");
272        mResultRecorder.noResult(file);
273    }
274
275    private void processResult(String testFile, String actualResultFile, String expectedResultFile, boolean ignoreResult) {
276        Log.v(LOGTAG, "  Processing result: " + testFile);
277
278        if (ignoreResult) {
279            ignoreResultCase(testFile);
280            return;
281        }
282
283        File actual = new File(actualResultFile);
284        File expected = new File(expectedResultFile);
285        if (actual.exists() && expected.exists()) {
286            try {
287                if (FsUtils.diffIgnoreSpaces(actualResultFile, expectedResultFile)) {
288                    passedCase(testFile);
289                } else {
290                    failedCase(testFile);
291                }
292            } catch (FileNotFoundException ex) {
293                Log.e(LOGTAG, "File not found : " + ex.getMessage());
294            } catch (IOException ex) {
295                Log.e(LOGTAG, "IO Error : " + ex.getMessage());
296            }
297            return;
298        }
299
300        if (!expected.exists()) {
301            noResultCase(testFile);
302        }
303    }
304
305    private void runTestAndWaitUntilDone(TestShellActivity activity, String test, int timeout, boolean ignoreResult, int testNumber) {
306        activity.setCallback(new TestShellCallback() {
307            public void finished() {
308                synchronized (LayoutTestsAutoTest.this) {
309                    mFinished = true;
310                    LayoutTestsAutoTest.this.notifyAll();
311                }
312            }
313
314            public void timedOut(String url) {
315                Log.v(LOGTAG, "layout timeout: " + url);
316            }
317
318            @Override
319            public void dumpResult(String webViewDump) {
320            }
321        });
322
323        String resultFile = getResultFile(test);
324        if (resultFile == null) {
325            // Simply ignore this test.
326            return;
327        }
328        if (mRebaselineResults) {
329            String expectedResultFile = getExpectedResultFile(test);
330            File f = new File(expectedResultFile);
331            if (f.exists()) {
332                return;  // don't run test and don't overwrite default tests.
333            }
334
335            resultFile = getAndroidExpectedResultFile(expectedResultFile);
336        }
337
338        mFinished = false;
339        Intent intent = new Intent(Intent.ACTION_VIEW);
340        intent.setClass(activity, TestShellActivity.class);
341        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
342        intent.putExtra(TestShellActivity.TEST_URL, FsUtils.getTestUrl(test));
343        intent.putExtra(TestShellActivity.RESULT_FILE, resultFile);
344        intent.putExtra(TestShellActivity.TIMEOUT_IN_MILLIS, timeout);
345        intent.putExtra(TestShellActivity.TOTAL_TEST_COUNT, mTestCount);
346        intent.putExtra(TestShellActivity.CURRENT_TEST_NUMBER, testNumber);
347        intent.putExtra(TestShellActivity.STOP_ON_REF_ERROR, true);
348        activity.startActivity(intent);
349
350        // Wait until done.
351        synchronized (this) {
352            while(!mFinished){
353                try {
354                    this.wait();
355                } catch (InterruptedException e) { }
356            }
357        }
358
359        if (!mRebaselineResults) {
360            String expectedResultFile = getExpectedResultFile(test);
361            File f = new File(expectedResultFile);
362            if (!f.exists()) {
363                expectedResultFile = getAndroidExpectedResultFile(expectedResultFile);
364            }
365
366            processResult(test, resultFile, expectedResultFile, ignoreResult);
367        }
368    }
369
370    // Invokes running of layout tests
371    // and waits till it has finished running.
372    public void executeLayoutTests(boolean resume) {
373        LayoutTestsAutoRunner runner = (LayoutTestsAutoRunner) getInstrumentation();
374        // A convenient method to be called by another activity.
375
376        if (runner.mTestPath == null) {
377            Log.e(LOGTAG, "No test specified");
378            return;
379        }
380
381        this.mTestList = new Vector<String>();
382        this.mTestListIgnoreResult = new Vector<Boolean>();
383
384        // Read settings
385        mTestPathPrefix = (new File(LAYOUT_TESTS_ROOT + runner.mTestPath)).getAbsolutePath();
386        mRebaselineResults = runner.mRebaseline;
387        // V8 is the default JavaScript engine.
388        mJsEngine = runner.mJsEngine == null ? "v8" : runner.mJsEngine;
389
390        int timeout = runner.mTimeoutInMillis;
391        if (timeout <= 0) {
392            timeout = DEFAULT_TIMEOUT_IN_MILLIS;
393        }
394
395        this.mResultRecorder = new MyTestRecorder(resume);
396
397        if (!resume)
398            clearTestStatus();
399
400        getTestList();
401        if (resume)
402            resumeTestList();
403
404        TestShellActivity activity = getActivity();
405        activity.setDefaultDumpDataType(DumpDataType.EXT_REPR);
406
407        // Run tests.
408        for (int i = 0; i < mTestList.size(); i++) {
409            String s = mTestList.elementAt(i);
410            boolean ignoreResult = mTestListIgnoreResult.elementAt(i);
411            FsUtils.updateTestStatus(TEST_STATUS_FILE, s);
412            // Run tests
413            // i is 0 based, but test count is 1 based so add 1 to i here.
414            runTestAndWaitUntilDone(activity, s, runner.mTimeoutInMillis, ignoreResult,
415                    i + 1 + mResumeIndex);
416        }
417
418        FsUtils.updateTestStatus(TEST_STATUS_FILE, "#DONE");
419        ForwardService.getForwardService().stopForwardService();
420        activity.finish();
421    }
422
423    private String getTestPath() {
424        LayoutTestsAutoRunner runner = (LayoutTestsAutoRunner) getInstrumentation();
425
426        String test_path = LAYOUT_TESTS_ROOT;
427        if (runner.mTestPath != null) {
428            test_path += runner.mTestPath;
429        }
430        test_path = new File(test_path).getAbsolutePath();
431        Log.v("LayoutTestsAutoTest", " Test path : " + test_path);
432
433        return test_path;
434    }
435
436    public void generateTestList() {
437        try {
438            File tests_list = new File(LAYOUT_TESTS_LIST_FILE);
439            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tests_list, false));
440            FsUtils.writeLayoutTestListRecursively(bos, getTestPath(), false); // Don't ignore results
441            bos.flush();
442            bos.close();
443       } catch (Exception e) {
444           Log.e(LOGTAG, "Error when creating test list: " + e.getMessage());
445       }
446    }
447
448    // Running all the layout tests at once sometimes
449    // causes the dumprendertree to run out of memory.
450    // So, additional tests are added to run the tests
451    // in chunks.
452    public void startLayoutTests() {
453        try {
454            File tests_list = new File(LAYOUT_TESTS_LIST_FILE);
455            if (!tests_list.exists())
456              generateTestList();
457        } catch (Exception e) {
458            e.printStackTrace();
459        }
460
461        executeLayoutTests(false);
462    }
463
464    public void resumeLayoutTests() {
465        executeLayoutTests(true);
466    }
467
468    public void copyResultsAndRunnerAssetsToCache() {
469        try {
470            Context targetContext = getInstrumentation().getTargetContext();
471            File cacheDir = targetContext.getCacheDir();
472
473            for( int i=0; i< LAYOUT_TESTS_RESULTS_REFERENCE_FILES.length; i++) {
474                InputStream in = targetContext.getAssets().open(
475                        LAYOUT_TESTS_RESULTS_REFERENCE_FILES[i]);
476                OutputStream out = new FileOutputStream(new File(cacheDir,
477                        LAYOUT_TESTS_RESULTS_REFERENCE_FILES[i]));
478
479                byte[] buf = new byte[2048];
480                int len;
481
482                while ((len = in.read(buf)) >= 0 ) {
483                    out.write(buf, 0, len);
484                }
485                out.close();
486                in.close();
487            }
488        }catch (IOException e) {
489          e.printStackTrace();
490        }
491
492    }
493
494}
495