Summarizer.java revision 603b70dd8b1ef8acabf71c52089c6cd5396fe931
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.content.res.AssetManager;
20import android.content.res.Configuration;
21import android.content.res.Resources;
22import android.os.Build;
23import android.util.DisplayMetrics;
24import android.util.Log;
25
26import com.android.dumprendertree2.forwarder.ForwarderManager;
27
28import java.io.File;
29import java.net.MalformedURLException;
30import java.net.URL;
31import java.text.SimpleDateFormat;
32import java.util.ArrayList;
33import java.util.Collections;
34import java.util.Date;
35import java.util.List;
36import java.util.regex.Matcher;
37import java.util.regex.Pattern;
38
39/**
40 * A class that collects information about tests that ran and can create HTML
41 * files with summaries and easy navigation.
42 */
43public class Summarizer {
44
45    private static final String LOG_TAG = "Summarizer";
46
47    private static final String CSS =
48            "<style type=\"text/css\">" +
49            "* {" +
50            "       font-family: Verdana;" +
51            "       border: 0;" +
52            "       margin: 0;" +
53            "       padding: 0;}" +
54            "body {" +
55            "       margin: 10px;}" +
56            "h1 {" +
57            "       font-size: 24px;" +
58            "       margin: 4px 0 4px 0;}" +
59            "h2 {" +
60            "       font-size:18px;" +
61            "       text-transform: uppercase;" +
62            "       margin: 20px 0 3px 0;}" +
63            "h3, h3 a {" +
64            "       font-size: 14px;" +
65            "       color: black;" +
66            "       text-decoration: none;" +
67            "       margin-bottom: 4px;}" +
68            "h3 a span.path {" +
69            "       text-decoration: underline;}" +
70            "h3 span.tri {" +
71            "       text-decoration: none;" +
72            "       float: left;" +
73            "       width: 20px;}" +
74            "h3 span.sqr {" +
75            "       text-decoration: none;" +
76            "       color: #8ee100;" +
77            "       float: left;" +
78            "       width: 20px;}" +
79            "h3 img {" +
80            "       width: 8px;" +
81            "       margin-right: 4px;}" +
82            "div.diff {" +
83            "       margin-bottom: 25px;}" +
84            "div.diff a {" +
85            "       font-size: 12px;" +
86            "       color: #888;}" +
87            "table.visual_diff {" +
88            "       border-bottom: 0px solid;" +
89            "       border-collapse: collapse;" +
90            "       width: 100%;" +
91            "       margin-bottom: 2px;}" +
92            "table.visual_diff tr.headers td {" +
93            "       border-bottom: 1px solid;" +
94            "       border-top: 0;" +
95            "       padding-bottom: 3px;}" +
96            "table.visual_diff tr.results td {" +
97            "       border-top: 1px dashed;" +
98            "       border-right: 1px solid;" +
99            "       font-size: 15px;" +
100            "       vertical-align: top;}" +
101            "table.visual_diff tr.results td.line_count {" +
102            "       background-color:#aaa;" +
103            "       min-width:20px;" +
104            "       text-align: right;" +
105            "       border-right: 1px solid;" +
106            "       border-left: 1px solid;" +
107            "       padding: 2px 1px 2px 0px;}" +
108            "table.visual_diff tr.results td.line {" +
109            "       padding: 2px 0px 2px 4px;" +
110            "       border-right: 1px solid;" +
111            "       width: 49.8%;}" +
112            "table.visual_diff tr.footers td {" +
113            "       border-top: 1px solid;" +
114            "       border-bottom: 0;}" +
115            "table.visual_diff tr td.space {" +
116            "       border: 0;" +
117            "       width: 0.4%}" +
118            "div.space {" +
119            "       margin-top:4px;}" +
120            "span.eql {" +
121            "       background-color: #f3f3f3;}" +
122            "span.del {" +
123            "       background-color: #ff8888; }" +
124            "span.ins {" +
125            "       background-color: #88ff88; }" +
126            "span.fail {" +
127            "       color: red;}" +
128            "span.pass {" +
129            "       color: green;}" +
130            "span.time_out {" +
131            "       color: orange;}" +
132            "table.summary {" +
133            "       border: 1px solid black;" +
134            "       margin-top: 20px;}" +
135            "table.summary td {" +
136            "       padding: 3px;}" +
137            "span.listItem {" +
138            "       font-size: 11px;" +
139            "       font-weight: normal;" +
140            "       text-transform: uppercase;" +
141            "       padding: 3px;" +
142            "       -webkit-border-radius: 4px;}" +
143            "span." + AbstractResult.ResultCode.PASS.name() + "{" +
144            "       background-color: #8ee100;" +
145            "       color: black;}" +
146            "span." + AbstractResult.ResultCode.FAIL_RESULT_DIFFERS.name() + "{" +
147            "       background-color: #ccc;" +
148            "       color: black;}" +
149            "span." + AbstractResult.ResultCode.FAIL_NO_EXPECTED_RESULT.name() + "{" +
150            "       background-color: #a700e4;" +
151            "       color: #fff;}" +
152            "span." + AbstractResult.ResultCode.FAIL_TIMED_OUT.name() + "{" +
153            "       background-color: #f3cb00;" +
154            "       color: black;}" +
155            "span." + AbstractResult.ResultCode.FAIL_CRASHED.name() + "{" +
156            "       background-color: #c30000;" +
157            "       color: #fff;}" +
158            "span.noLtc {" +
159            "       background-color: #944000;" +
160            "       color: #fff;}" +
161            "span.noEventSender {" +
162            "       background-color: #815600;" +
163            "       color: #fff;}" +
164            "</style>";
165
166    private static final String SCRIPT =
167            "<script type=\"text/javascript\">" +
168            "    function toggleDisplay(id) {" +
169            "        element = document.getElementById(id);" +
170            "        triangle = document.getElementById('tri.' + id);" +
171            "        if (element.style.display == 'none') {" +
172            "            element.style.display = 'inline';" +
173            "            triangle.innerHTML = '&#x25bc; ';" +
174            "        } else {" +
175            "            element.style.display = 'none';" +
176            "            triangle.innerHTML = '&#x25b6; ';" +
177            "        }" +
178            "    }" +
179            "</script>";
180
181    /** TODO: Make it a setting */
182    private static final String HTML_DETAILS_RELATIVE_PATH = "details.html";
183    private static final String TXT_SUMMARY_RELATIVE_PATH = "summary.txt";
184
185    private int mCrashedTestsCount = 0;
186    private List<AbstractResult> mFailedNotIgnoredTests = new ArrayList<AbstractResult>();
187    private List<AbstractResult> mIgnoredTests = new ArrayList<AbstractResult>();
188    private List<String> mPassedNotIgnoredTests = new ArrayList<String>();
189
190    private FileFilter mFileFilter;
191    private String mResultsRootDirPath;
192
193    private String mTestsRelativePath;
194
195    private Date mDate;
196
197    public Summarizer(FileFilter fileFilter, String resultsRootDirPath) {
198        mFileFilter = fileFilter;
199        mResultsRootDirPath = resultsRootDirPath;
200    }
201
202    public void appendTest(AbstractResult result) {
203        String relativePath = result.getRelativePath();
204
205        if (result.getResultCode() == AbstractResult.ResultCode.FAIL_CRASHED) {
206            mCrashedTestsCount++;
207        }
208
209        if (mFileFilter.isIgnoreRes(relativePath)) {
210            mIgnoredTests.add(result);
211        } else if (result.getResultCode() == AbstractResult.ResultCode.PASS) {
212            mPassedNotIgnoredTests.add(relativePath);
213        } else {
214            mFailedNotIgnoredTests.add(result);
215        }
216    }
217
218    public void setTestsRelativePath(String testsRelativePath) {
219        mTestsRelativePath = testsRelativePath;
220    }
221
222    public void summarize() {
223        createHtmlDetails();
224        createTxtSummary();
225    }
226
227    public void reset() {
228        mCrashedTestsCount = 0;
229        mFailedNotIgnoredTests.clear();
230        mIgnoredTests.clear();
231        mPassedNotIgnoredTests.clear();
232        mDate = new Date();
233    }
234
235    private void createTxtSummary() {
236        StringBuilder txt = new StringBuilder();
237
238        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
239        txt.append(mTestsRelativePath + "\n");
240        txt.append("Date: " + dateFormat.format(mDate) + "\n");
241        txt.append("Build fingerprint: " + Build.FINGERPRINT + "\n");
242        txt.append("WebKit version: " + getWebKitVersionFromUserAgentString() + "\n");
243
244        txt.append("TOTAL:   " + getTotalTestCount() + "\n");
245        if (mCrashedTestsCount > 0) {
246            txt.append("CRASHED (total among all tests): " + mCrashedTestsCount + "\n");
247            txt.append("-------------");
248        }
249        txt.append("FAILED:  " + mFailedNotIgnoredTests.size() + "\n");
250        txt.append("IGNORED: " + mIgnoredTests.size() + "\n");
251        txt.append("PASSED:  " + mPassedNotIgnoredTests.size() + "\n");
252
253        FsUtils.writeDataToStorage(new File(mResultsRootDirPath, TXT_SUMMARY_RELATIVE_PATH),
254                txt.toString().getBytes(), false);
255    }
256
257    private void createHtmlDetails() {
258        StringBuilder html = new StringBuilder();
259
260        html.append("<html><head>");
261        html.append(CSS);
262        html.append(SCRIPT);
263        html.append("</head><body>");
264
265        createTopSummaryTable(html);
266
267        createResultsListWithDiff(html, "Failed", mFailedNotIgnoredTests);
268
269        createResultsListWithDiff(html, "Ignored", mIgnoredTests);
270
271        createResultsListNoDiff(html, "Passed", mPassedNotIgnoredTests);
272
273        html.append("</body></html>");
274
275        FsUtils.writeDataToStorage(new File(mResultsRootDirPath, HTML_DETAILS_RELATIVE_PATH),
276                html.toString().getBytes(), false);
277    }
278
279    private int getTotalTestCount() {
280        return mFailedNotIgnoredTests.size() +
281                mPassedNotIgnoredTests.size() +
282                mIgnoredTests.size();
283    }
284
285    private String getWebKitVersionFromUserAgentString() {
286        Resources resources = new Resources(new AssetManager(), new DisplayMetrics(),
287                new Configuration());
288        String userAgent =
289                resources.getString(com.android.internal.R.string.web_user_agent);
290
291        Matcher matcher = Pattern.compile("AppleWebKit/([0-9]+?\\.[0-9])").matcher(userAgent);
292        if (matcher.find()) {
293            return matcher.group(1);
294        }
295        return "unknown";
296    }
297
298    private void createTopSummaryTable(StringBuilder html) {
299        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
300        html.append("<h1>" + mTestsRelativePath + "</h1>");
301        html.append("<h3>" + "Date: " + dateFormat.format(new Date()) + "</h3>");
302        html.append("<h3>" + "Build fingerprint: " + Build.FINGERPRINT + "</h3>");
303        html.append("<h3>" + "WebKit version: " + getWebKitVersionFromUserAgentString() + "</h3>");
304
305        html.append("<table class=\"summary\">");
306        createSummaryTableRow(html, "TOTAL", getTotalTestCount());
307        createSummaryTableRow(html, "CRASHED", mCrashedTestsCount);
308        createSummaryTableRow(html, "FAILED", mFailedNotIgnoredTests.size());
309        createSummaryTableRow(html, "IGNORED", mIgnoredTests.size());
310        createSummaryTableRow(html, "PASSED", mPassedNotIgnoredTests.size());
311        html.append("</table>");
312    }
313
314    private void createSummaryTableRow(StringBuilder html, String caption, int size) {
315        html.append("<tr>");
316        html.append("    <td>" + caption + "</td>");
317        html.append("    <td>" + size + "</td>");
318        html.append("</tr>");
319    }
320
321    private void createResultsListWithDiff(StringBuilder html, String title,
322            List<AbstractResult> resultsList) {
323        String relativePath;
324        String id = "";
325        AbstractResult.ResultCode resultCode;
326
327        Collections.sort(resultsList);
328        html.append("<h2>" + title + " [" + resultsList.size() + "]</h2>");
329        for (AbstractResult result : resultsList) {
330            relativePath = result.getRelativePath();
331            resultCode = result.getResultCode();
332
333            html.append("<h3>");
334
335            if (resultCode == AbstractResult.ResultCode.PASS) {
336                html.append("<span class=\"sqr\">&#x25a0; </span>");
337                html.append("<span class=\"path\">" + relativePath + "</span>");
338            } else {
339                /**
340                 * Technically, two different paths could end up being the same, because
341                 * ':' is a valid  character in a path. However, it is probably not going
342                 * to cause any problems in this case
343                 */
344                id = relativePath.replace(File.separator, ":");
345                html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');");
346                html.append("return false;\">");
347                html.append("<span class=\"tri\" id=\"tri." + id + "\">&#x25b6; </span>");
348                html.append("<span class=\"path\">" + relativePath + "</span>");
349                html.append("</a>");
350            }
351
352            html.append(" <span class=\"listItem " + resultCode.name() + "\">");
353            html.append(resultCode.toString());
354            html.append("</span>");
355
356            /** Detect missing LTC function */
357            String additionalTextOutputString = result.getAdditionalTextOutputString();
358            if (additionalTextOutputString != null &&
359                    additionalTextOutputString.contains("com.android.dumprendertree") &&
360                    additionalTextOutputString.contains("has no method")) {
361                if (additionalTextOutputString.contains("LayoutTestController")) {
362                    html.append(" <span class=\"listItem noLtc\">LTC function missing</span>");
363                }
364                if (additionalTextOutputString.contains("EventSender")) {
365                    html.append(" <span class=\"listItem noEventSender\">");
366                    html.append("ES function missing</span>");
367                }
368            }
369
370            html.append("</h3>");
371
372            if (resultCode != AbstractResult.ResultCode.PASS) {
373                html.append("<div class=\"diff\" style=\"display: none;\" id=\"" + id + "\">");
374                html.append(result.getDiffAsHtml());
375                html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');");
376                html.append("return false;\">Hide</a>");
377                html.append(" | ");
378                html.append("<a href=\"" + getViewSourceUrl(relativePath).toString() + "\"");
379                html.append(" target=\"_blank\">Show source</a>");
380                html.append("</div>");
381            }
382
383            html.append("<div class=\"space\"></div>");
384        }
385    }
386
387    private void createResultsListNoDiff(StringBuilder html, String title,
388            List<String> resultsList) {
389        Collections.sort(resultsList);
390        html.append("<h2>Passed [" + resultsList.size() + "]</h2>");
391        for (String result : resultsList) {
392            html.append("<h3>");
393            html.append("<a href=\"" + getViewSourceUrl(result).toString() + "\"");
394            html.append(" target=\"_blank\">");
395            html.append("<span class=\"sqr\">&#x25a0; </span>");
396            html.append("<span class=\"path\">" + result + "</span>");
397            html.append("</a>");
398            html.append("</h3>");
399            html.append("<div class=\"space\"></div>");
400        }
401    }
402
403    private static final URL getViewSourceUrl(String relativePath) {
404        URL url = null;
405        try {
406            url = new URL("http", "localhost", ForwarderManager.HTTP_PORT,
407                    "/WebKitTools/DumpRenderTree/android/view_source.php?src=" +
408                    relativePath);
409        } catch (MalformedURLException e) {
410            assert false : "relativePath=" + relativePath;
411        }
412        return url;
413    }
414}