| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.dumprendertree2; |
| |
| import android.content.Context; |
| import android.content.res.AssetManager; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.os.Build; |
| import android.os.Message; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| |
| import com.android.dumprendertree2.forwarder.ForwarderManager; |
| |
| import java.io.File; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URL; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * A class that collects information about tests that ran and can create HTML |
| * files with summaries and easy navigation. |
| */ |
| public class Summarizer { |
| |
| private static final String LOG_TAG = "Summarizer"; |
| |
| private static final String CSS = |
| "<style type=\"text/css\">" + |
| "* {" + |
| " font-family: Verdana;" + |
| " border: 0;" + |
| " margin: 0;" + |
| " padding: 0;}" + |
| "body {" + |
| " margin: 10px;}" + |
| "h1 {" + |
| " font-size: 24px;" + |
| " margin: 4px 0 4px 0;}" + |
| "h2 {" + |
| " font-size:18px;" + |
| " text-transform: uppercase;" + |
| " margin: 20px 0 3px 0;}" + |
| "h3, h3 a {" + |
| " font-size: 14px;" + |
| " color: black;" + |
| " text-decoration: none;" + |
| " margin-top: 4px;" + |
| " margin-bottom: 2px;}" + |
| "h3 a span.path {" + |
| " text-decoration: underline;}" + |
| "h3 span.tri {" + |
| " text-decoration: none;" + |
| " float: left;" + |
| " width: 20px;}" + |
| "h3 span.sqr {" + |
| " text-decoration: none;" + |
| " float: left;" + |
| " width: 20px;}" + |
| "h3 span.sqr_pass {" + |
| " color: #8ee100;}" + |
| "h3 span.sqr_fail {" + |
| " color: #c30000;}" + |
| "span.source {" + |
| " display: block;" + |
| " font-size: 10px;" + |
| " color: #888;" + |
| " margin-left: 20px;" + |
| " margin-bottom: 1px;}" + |
| "span.source a {" + |
| " font-size: 10px;" + |
| " color: #888;}" + |
| "h3 img {" + |
| " width: 8px;" + |
| " margin-right: 4px;}" + |
| "div.diff {" + |
| " margin-bottom: 25px;}" + |
| "div.diff a {" + |
| " font-size: 12px;" + |
| " color: #888;}" + |
| "table.visual_diff {" + |
| " border-bottom: 0px solid;" + |
| " border-collapse: collapse;" + |
| " width: 100%;" + |
| " margin-bottom: 2px;}" + |
| "table.visual_diff tr.headers td {" + |
| " border-bottom: 1px solid;" + |
| " border-top: 0;" + |
| " padding-bottom: 3px;}" + |
| "table.visual_diff tr.results td {" + |
| " border-top: 1px dashed;" + |
| " border-right: 1px solid;" + |
| " font-size: 15px;" + |
| " vertical-align: top;}" + |
| "table.visual_diff tr.results td.line_count {" + |
| " background-color:#aaa;" + |
| " min-width:20px;" + |
| " text-align: right;" + |
| " border-right: 1px solid;" + |
| " border-left: 1px solid;" + |
| " padding: 2px 1px 2px 0px;}" + |
| "table.visual_diff tr.results td.line {" + |
| " padding: 2px 0px 2px 4px;" + |
| " border-right: 1px solid;" + |
| " width: 49.8%;}" + |
| "table.visual_diff tr.footers td {" + |
| " border-top: 1px solid;" + |
| " border-bottom: 0;}" + |
| "table.visual_diff tr td.space {" + |
| " border: 0;" + |
| " width: 0.4%}" + |
| "div.space {" + |
| " margin-top:4px;}" + |
| "span.eql {" + |
| " background-color: #f3f3f3;}" + |
| "span.del {" + |
| " background-color: #ff8888; }" + |
| "span.ins {" + |
| " background-color: #88ff88; }" + |
| "table.summary {" + |
| " border: 1px solid black;" + |
| " margin-top: 20px;}" + |
| "table.summary td {" + |
| " padding: 3px;}" + |
| "span.listItem {" + |
| " font-size: 11px;" + |
| " font-weight: normal;" + |
| " text-transform: uppercase;" + |
| " padding: 3px;" + |
| " -webkit-border-radius: 4px;}" + |
| "span." + AbstractResult.ResultCode.RESULTS_DIFFER.name() + "{" + |
| " background-color: #ccc;" + |
| " color: black;}" + |
| "span." + AbstractResult.ResultCode.NO_EXPECTED_RESULT.name() + "{" + |
| " background-color: #a700e4;" + |
| " color: #fff;}" + |
| "span.timed_out {" + |
| " background-color: #f3cb00;" + |
| " color: black;}" + |
| "span.crashed {" + |
| " background-color: #c30000;" + |
| " color: #fff;}" + |
| "span.noLtc {" + |
| " background-color: #944000;" + |
| " color: #fff;}" + |
| "span.noEventSender {" + |
| " background-color: #815600;" + |
| " color: #fff;}" + |
| "</style>"; |
| |
| private static final String SCRIPT = |
| "<script type=\"text/javascript\">" + |
| " function toggleDisplay(id) {" + |
| " element = document.getElementById(id);" + |
| " triangle = document.getElementById('tri.' + id);" + |
| " if (element.style.display == 'none') {" + |
| " element.style.display = 'inline';" + |
| " triangle.innerHTML = '▼ ';" + |
| " } else {" + |
| " element.style.display = 'none';" + |
| " triangle.innerHTML = '▶ ';" + |
| " }" + |
| " }" + |
| "</script>"; |
| |
| /** TODO: Make it a setting */ |
| private static final String HTML_DETAILS_RELATIVE_PATH = "details.html"; |
| private static final String TXT_SUMMARY_RELATIVE_PATH = "summary.txt"; |
| |
| private static final int RESULTS_PER_DUMP = 500; |
| private static final int RESULTS_PER_DB_ACCESS = 50; |
| |
| private int mCrashedTestsCount = 0; |
| private List<AbstractResult> mUnexpectedFailures = new ArrayList<AbstractResult>(); |
| private List<AbstractResult> mExpectedFailures = new ArrayList<AbstractResult>(); |
| private List<AbstractResult> mExpectedPasses = new ArrayList<AbstractResult>(); |
| private List<AbstractResult> mUnexpectedPasses = new ArrayList<AbstractResult>(); |
| |
| private Cursor mUnexpectedFailuresCursor; |
| private Cursor mExpectedFailuresCursor; |
| private Cursor mUnexpectedPassesCursor; |
| private Cursor mExpectedPassesCursor; |
| |
| private FileFilter mFileFilter; |
| private String mResultsRootDirPath; |
| private String mTestsRelativePath; |
| private Date mDate; |
| |
| private int mResultsSinceLastHtmlDump = 0; |
| private int mResultsSinceLastDbAccess = 0; |
| |
| private SummarizerDBHelper mDbHelper; |
| |
| public Summarizer(FileFilter fileFilter, String resultsRootDirPath, Context context) { |
| mFileFilter = fileFilter; |
| mResultsRootDirPath = resultsRootDirPath; |
| |
| /** |
| * We don't run the database I/O in a separate thread to avoid consumer/producer problem |
| * and to simplify code. |
| */ |
| mDbHelper = new SummarizerDBHelper(context); |
| mDbHelper.open(); |
| } |
| |
| public static URI getDetailsUri() { |
| return new File(ManagerService.RESULTS_ROOT_DIR_PATH + File.separator + |
| HTML_DETAILS_RELATIVE_PATH).toURI(); |
| } |
| |
| public void appendTest(AbstractResult result) { |
| String relativePath = result.getRelativePath(); |
| |
| if (result.didCrash()) { |
| mCrashedTestsCount++; |
| } |
| |
| if (result.didPass()) { |
| result.clearResults(); |
| if (mFileFilter.isFail(relativePath)) { |
| mUnexpectedPasses.add(result); |
| } else { |
| mExpectedPasses.add(result); |
| } |
| } else { |
| if (mFileFilter.isFail(relativePath)) { |
| mExpectedFailures.add(result); |
| } else { |
| mUnexpectedFailures.add(result); |
| } |
| } |
| |
| if (++mResultsSinceLastDbAccess == RESULTS_PER_DB_ACCESS) { |
| persistLists(); |
| clearLists(); |
| } |
| } |
| |
| private void clearLists() { |
| mUnexpectedFailures.clear(); |
| mExpectedFailures.clear(); |
| mUnexpectedPasses.clear(); |
| mExpectedPasses.clear(); |
| } |
| |
| private void persistLists() { |
| persistListToTable(mUnexpectedFailures, SummarizerDBHelper.UNEXPECTED_FAILURES_TABLE); |
| persistListToTable(mExpectedFailures, SummarizerDBHelper.EXPECTED_FAILURES_TABLE); |
| persistListToTable(mUnexpectedPasses, SummarizerDBHelper.UNEXPECTED_PASSES_TABLE); |
| persistListToTable(mExpectedPasses, SummarizerDBHelper.EXPECTED_PASSES_TABLE); |
| mResultsSinceLastDbAccess = 0; |
| } |
| |
| private void persistListToTable(List<AbstractResult> results, String table) { |
| for (AbstractResult abstractResult : results) { |
| mDbHelper.insertAbstractResult(abstractResult, table); |
| } |
| } |
| |
| public void setTestsRelativePath(String testsRelativePath) { |
| mTestsRelativePath = testsRelativePath; |
| } |
| |
| public void summarize(Message onFinishMessage) { |
| persistLists(); |
| clearLists(); |
| |
| mUnexpectedFailuresCursor = |
| mDbHelper.getAbstractResults(SummarizerDBHelper.UNEXPECTED_FAILURES_TABLE); |
| mUnexpectedPassesCursor = |
| mDbHelper.getAbstractResults(SummarizerDBHelper.UNEXPECTED_PASSES_TABLE); |
| mExpectedFailuresCursor = |
| mDbHelper.getAbstractResults(SummarizerDBHelper.EXPECTED_FAILURES_TABLE); |
| mExpectedPassesCursor = |
| mDbHelper.getAbstractResults(SummarizerDBHelper.EXPECTED_PASSES_TABLE); |
| |
| String webKitRevision = getWebKitRevision(); |
| createHtmlDetails(webKitRevision); |
| createTxtSummary(webKitRevision); |
| |
| clearLists(); |
| mUnexpectedFailuresCursor.close(); |
| mUnexpectedPassesCursor.close(); |
| mExpectedFailuresCursor.close(); |
| mExpectedPassesCursor.close(); |
| |
| onFinishMessage.sendToTarget(); |
| } |
| |
| public void reset() { |
| mCrashedTestsCount = 0; |
| clearLists(); |
| mDbHelper.reset(); |
| mDate = new Date(); |
| } |
| |
| private void dumpHtmlToFile(StringBuilder html, boolean append) { |
| FsUtils.writeDataToStorage(new File(mResultsRootDirPath, HTML_DETAILS_RELATIVE_PATH), |
| html.toString().getBytes(), append); |
| html.setLength(0); |
| mResultsSinceLastHtmlDump = 0; |
| } |
| |
| private void createTxtSummary(String webKitRevision) { |
| StringBuilder txt = new StringBuilder(); |
| |
| SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); |
| txt.append("Path: " + mTestsRelativePath + "\n"); |
| txt.append("Date: " + dateFormat.format(mDate) + "\n"); |
| txt.append("Build fingerprint: " + Build.FINGERPRINT + "\n"); |
| txt.append("WebKit version: " + getWebKitVersionFromUserAgentString() + "\n"); |
| txt.append("WebKit revision: " + webKitRevision + "\n"); |
| |
| txt.append("TOTAL: " + getTotalTestCount() + "\n"); |
| txt.append("CRASHED (among all tests): " + mCrashedTestsCount + "\n"); |
| txt.append("UNEXPECTED FAILURES: " + mUnexpectedFailuresCursor.getCount() + "\n"); |
| txt.append("UNEXPECTED PASSES: " + mUnexpectedPassesCursor.getCount() + "\n"); |
| txt.append("EXPECTED FAILURES: " + mExpectedFailuresCursor.getCount() + "\n"); |
| txt.append("EXPECTED PASSES: " + mExpectedPassesCursor.getCount() + "\n"); |
| |
| FsUtils.writeDataToStorage(new File(mResultsRootDirPath, TXT_SUMMARY_RELATIVE_PATH), |
| txt.toString().getBytes(), false); |
| } |
| |
| private void createHtmlDetails(String webKitRevision) { |
| StringBuilder html = new StringBuilder(); |
| |
| html.append("<html><head>"); |
| html.append(CSS); |
| html.append(SCRIPT); |
| html.append("</head><body>"); |
| |
| createTopSummaryTable(webKitRevision, html); |
| dumpHtmlToFile(html, false); |
| |
| createResultsList(html, "Unexpected failures", mUnexpectedFailuresCursor); |
| createResultsList(html, "Unexpected passes", mUnexpectedPassesCursor); |
| createResultsList(html, "Expected failures", mExpectedFailuresCursor); |
| createResultsList(html, "Expected passes", mExpectedPassesCursor); |
| |
| html.append("</body></html>"); |
| dumpHtmlToFile(html, true); |
| } |
| |
| private int getTotalTestCount() { |
| return mUnexpectedFailuresCursor.getCount() + |
| mUnexpectedPassesCursor.getCount() + |
| mExpectedPassesCursor.getCount() + |
| mExpectedFailuresCursor.getCount(); |
| } |
| |
| private String getWebKitVersionFromUserAgentString() { |
| Resources resources = new Resources(new AssetManager(), new DisplayMetrics(), |
| new Configuration()); |
| String userAgent = |
| resources.getString(com.android.internal.R.string.web_user_agent); |
| |
| Matcher matcher = Pattern.compile("AppleWebKit/([0-9]+?\\.[0-9])").matcher(userAgent); |
| if (matcher.find()) { |
| return matcher.group(1); |
| } |
| return "unknown"; |
| } |
| |
| private String getWebKitRevision() { |
| URL url = null; |
| try { |
| url = new URL(ForwarderManager.getHostSchemePort(false) + "ThirdPartyProject.prop"); |
| } catch (MalformedURLException e) { |
| assert false; |
| } |
| |
| String thirdPartyProjectContents = new String(FsUtils.readDataFromUrl(url)); |
| Matcher matcher = Pattern.compile("^version=([0-9]+)", Pattern.MULTILINE).matcher( |
| thirdPartyProjectContents); |
| if (matcher.find()) { |
| return matcher.group(1); |
| } |
| return "unknown"; |
| } |
| |
| private void createTopSummaryTable(String webKitRevision, StringBuilder html) { |
| SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); |
| html.append("<h1>" + "Layout tests' results for: " + |
| (mTestsRelativePath.equals("") ? "all tests" : mTestsRelativePath) + "</h1>"); |
| html.append("<h3>" + "Date: " + dateFormat.format(new Date()) + "</h3>"); |
| html.append("<h3>" + "Build fingerprint: " + Build.FINGERPRINT + "</h3>"); |
| html.append("<h3>" + "WebKit version: " + getWebKitVersionFromUserAgentString() + "</h3>"); |
| |
| html.append("<h3>" + "WebKit revision: "); |
| html.append("<a href=\"http://trac.webkit.org/browser/trunk?rev=" + webKitRevision + |
| "\" target=\"_blank\"><span class=\"path\">" + webKitRevision + "</span></a>"); |
| html.append("</h3>"); |
| |
| html.append("<table class=\"summary\">"); |
| createSummaryTableRow(html, "TOTAL", getTotalTestCount()); |
| createSummaryTableRow(html, "CRASHED (among all tests)", mCrashedTestsCount); |
| createSummaryTableRow(html, "UNEXPECTED FAILURES", mUnexpectedFailuresCursor.getCount()); |
| createSummaryTableRow(html, "UNEXPECTED PASSES", mUnexpectedPassesCursor.getCount()); |
| createSummaryTableRow(html, "EXPECTED FAILURES", mExpectedFailuresCursor.getCount()); |
| createSummaryTableRow(html, "EXPECTED PASSES", mExpectedPassesCursor.getCount()); |
| html.append("</table>"); |
| } |
| |
| private void createSummaryTableRow(StringBuilder html, String caption, int size) { |
| html.append("<tr>"); |
| html.append(" <td>" + caption + "</td>"); |
| html.append(" <td>" + size + "</td>"); |
| html.append("</tr>"); |
| } |
| |
| private void createResultsList( |
| StringBuilder html, String title, Cursor cursor) { |
| String relativePath; |
| String id = ""; |
| AbstractResult.ResultCode resultCode; |
| |
| html.append("<h2>" + title + " [" + cursor.getCount() + "]</h2>"); |
| |
| if (!cursor.moveToFirst()) { |
| return; |
| } |
| |
| AbstractResult result; |
| do { |
| result = SummarizerDBHelper.getAbstractResult(cursor); |
| |
| relativePath = result.getRelativePath(); |
| resultCode = result.getResultCode(); |
| |
| html.append("<h3>"); |
| |
| /** |
| * Technically, two different paths could end up being the same, because |
| * ':' is a valid character in a path. However, it is probably not going |
| * to cause any problems in this case |
| */ |
| id = relativePath.replace(File.separator, ":"); |
| |
| /** Write the test name */ |
| if (resultCode == AbstractResult.ResultCode.RESULTS_DIFFER) { |
| html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');"); |
| html.append("return false;\">"); |
| html.append("<span class=\"tri\" id=\"tri." + id + "\">▶ </span>"); |
| html.append("<span class=\"path\">" + relativePath + "</span>"); |
| html.append("</a>"); |
| } else { |
| html.append("<a href=\"" + getViewSourceUrl(result.getRelativePath()).toString() + "\""); |
| html.append(" target=\"_blank\">"); |
| html.append("<span class=\"sqr sqr_" + (result.didPass() ? "pass" : "fail")); |
| html.append("\">■ </span>"); |
| html.append("<span class=\"path\">" + result.getRelativePath() + "</span>"); |
| html.append("</a>"); |
| } |
| |
| if (!result.didPass()) { |
| appendTags(html, result); |
| } |
| |
| html.append("</h3>"); |
| appendExpectedResultsSources(result, html); |
| |
| if (resultCode == AbstractResult.ResultCode.RESULTS_DIFFER) { |
| html.append("<div class=\"diff\" style=\"display: none;\" id=\"" + id + "\">"); |
| html.append(result.getDiffAsHtml()); |
| html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');"); |
| html.append("return false;\">Hide</a>"); |
| html.append(" | "); |
| html.append("<a href=\"" + getViewSourceUrl(relativePath).toString() + "\""); |
| html.append(" target=\"_blank\">Show source</a>"); |
| html.append("</div>"); |
| } |
| |
| html.append("<div class=\"space\"></div>"); |
| |
| if (++mResultsSinceLastHtmlDump == RESULTS_PER_DUMP) { |
| dumpHtmlToFile(html, true); |
| } |
| |
| cursor.moveToNext(); |
| } while (!cursor.isAfterLast()); |
| } |
| |
| private void appendTags(StringBuilder html, AbstractResult result) { |
| /** Tag tests which crash, time out or where results don't match */ |
| if (result.didCrash()) { |
| html.append(" <span class=\"listItem crashed\">Crashed</span>"); |
| } else { |
| if (result.didTimeOut()) { |
| html.append(" <span class=\"listItem timed_out\">Timed out</span>"); |
| } |
| AbstractResult.ResultCode resultCode = result.getResultCode(); |
| if (resultCode != AbstractResult.ResultCode.RESULTS_MATCH) { |
| html.append(" <span class=\"listItem " + resultCode.name() + "\">"); |
| html.append(resultCode.toString()); |
| html.append("</span>"); |
| } |
| } |
| |
| /** Detect missing LTC function */ |
| String additionalTextOutputString = result.getAdditionalTextOutputString(); |
| if (additionalTextOutputString != null && |
| additionalTextOutputString.contains("com.android.dumprendertree") && |
| additionalTextOutputString.contains("has no method")) { |
| if (additionalTextOutputString.contains("LayoutTestController")) { |
| html.append(" <span class=\"listItem noLtc\">LTC function missing</span>"); |
| } |
| if (additionalTextOutputString.contains("EventSender")) { |
| html.append(" <span class=\"listItem noEventSender\">"); |
| html.append("ES function missing</span>"); |
| } |
| } |
| } |
| |
| private static final void appendExpectedResultsSources(AbstractResult result, |
| StringBuilder html) { |
| String textSource = result.getExpectedTextResultPath(); |
| String imageSource = result.getExpectedImageResultPath(); |
| |
| if (result.didCrash()) { |
| html.append("<span class=\"source\">Did not look for expected results</span>"); |
| return; |
| } |
| |
| if (textSource == null) { |
| // Show if a text result is missing. We may want to revisit this decision when we add |
| // support for image results. |
| html.append("<span class=\"source\">Expected textual result missing</span>"); |
| } else { |
| html.append("<span class=\"source\">Expected textual result from: "); |
| html.append("<a href=\"" + ForwarderManager.getHostSchemePort(false) + "LayoutTests/" + |
| textSource + "\""); |
| html.append(" target=\"_blank\">"); |
| html.append(textSource + "</a></span>"); |
| } |
| if (imageSource != null) { |
| html.append("<span class=\"source\">Expected image result from: "); |
| html.append("<a href=\"" + ForwarderManager.getHostSchemePort(false) + "LayoutTests/" + |
| imageSource + "\""); |
| html.append(" target=\"_blank\">"); |
| html.append(imageSource + "</a></span>"); |
| } |
| } |
| |
| private static final URL getViewSourceUrl(String relativePath) { |
| URL url = null; |
| try { |
| url = new URL("http", "localhost", ForwarderManager.HTTP_PORT, |
| "/WebKitTools/DumpRenderTree/android/view_source.php?src=" + |
| relativePath); |
| } catch (MalformedURLException e) { |
| assert false : "relativePath=" + relativePath; |
| } |
| return url; |
| } |
| } |