Home | History | Annotate | Download | only in dumprendertree2
      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 
     17 package com.android.dumprendertree2;
     18 
     19 import android.content.Context;
     20 import android.content.res.AssetManager;
     21 import android.content.res.Configuration;
     22 import android.content.res.Resources;
     23 import android.database.Cursor;
     24 import android.os.Build;
     25 import android.os.Message;
     26 import android.util.DisplayMetrics;
     27 import android.util.Log;
     28 
     29 import com.android.dumprendertree2.forwarder.ForwarderManager;
     30 
     31 import java.io.File;
     32 import java.net.MalformedURLException;
     33 import java.net.URI;
     34 import java.net.URL;
     35 import java.text.SimpleDateFormat;
     36 import java.util.ArrayList;
     37 import java.util.Date;
     38 import java.util.List;
     39 import java.util.regex.Matcher;
     40 import java.util.regex.Pattern;
     41 
     42 /**
     43  * A class that collects information about tests that ran and can create HTML
     44  * files with summaries and easy navigation.
     45  */
     46 public class Summarizer {
     47 
     48     private static final String LOG_TAG = "Summarizer";
     49 
     50     private static final String CSS =
     51             "<style type=\"text/css\">" +
     52             "* {" +
     53             "       font-family: Verdana;" +
     54             "       border: 0;" +
     55             "       margin: 0;" +
     56             "       padding: 0;}" +
     57             "body {" +
     58             "       margin: 10px;}" +
     59             "h1 {" +
     60             "       font-size: 24px;" +
     61             "       margin: 4px 0 4px 0;}" +
     62             "h2 {" +
     63             "       font-size:18px;" +
     64             "       text-transform: uppercase;" +
     65             "       margin: 20px 0 3px 0;}" +
     66             "h3, h3 a {" +
     67             "       font-size: 14px;" +
     68             "       color: black;" +
     69             "       text-decoration: none;" +
     70             "       margin-top: 4px;" +
     71             "       margin-bottom: 2px;}" +
     72             "h3 a span.path {" +
     73             "       text-decoration: underline;}" +
     74             "h3 span.tri {" +
     75             "       text-decoration: none;" +
     76             "       float: left;" +
     77             "       width: 20px;}" +
     78             "h3 span.sqr {" +
     79             "       text-decoration: none;" +
     80             "       float: left;" +
     81             "       width: 20px;}" +
     82             "h3 span.sqr_pass {" +
     83             "       color: #8ee100;}" +
     84             "h3 span.sqr_fail {" +
     85             "       color: #c30000;}" +
     86             "span.source {" +
     87             "       display: block;" +
     88             "       font-size: 10px;" +
     89             "       color: #888;" +
     90             "       margin-left: 20px;" +
     91             "       margin-bottom: 1px;}" +
     92             "span.source a {" +
     93             "       font-size: 10px;" +
     94             "       color: #888;}" +
     95             "h3 img {" +
     96             "       width: 8px;" +
     97             "       margin-right: 4px;}" +
     98             "div.diff {" +
     99             "       margin-bottom: 25px;}" +
    100             "div.diff a {" +
    101             "       font-size: 12px;" +
    102             "       color: #888;}" +
    103             "table.visual_diff {" +
    104             "       border-bottom: 0px solid;" +
    105             "       border-collapse: collapse;" +
    106             "       width: 100%;" +
    107             "       margin-bottom: 2px;}" +
    108             "table.visual_diff tr.headers td {" +
    109             "       border-bottom: 1px solid;" +
    110             "       border-top: 0;" +
    111             "       padding-bottom: 3px;}" +
    112             "table.visual_diff tr.results td {" +
    113             "       border-top: 1px dashed;" +
    114             "       border-right: 1px solid;" +
    115             "       font-size: 15px;" +
    116             "       vertical-align: top;}" +
    117             "table.visual_diff tr.results td.line_count {" +
    118             "       background-color:#aaa;" +
    119             "       min-width:20px;" +
    120             "       text-align: right;" +
    121             "       border-right: 1px solid;" +
    122             "       border-left: 1px solid;" +
    123             "       padding: 2px 1px 2px 0px;}" +
    124             "table.visual_diff tr.results td.line {" +
    125             "       padding: 2px 0px 2px 4px;" +
    126             "       border-right: 1px solid;" +
    127             "       width: 49.8%;}" +
    128             "table.visual_diff tr.footers td {" +
    129             "       border-top: 1px solid;" +
    130             "       border-bottom: 0;}" +
    131             "table.visual_diff tr td.space {" +
    132             "       border: 0;" +
    133             "       width: 0.4%}" +
    134             "div.space {" +
    135             "       margin-top:4px;}" +
    136             "span.eql {" +
    137             "       background-color: #f3f3f3;}" +
    138             "span.del {" +
    139             "       background-color: #ff8888; }" +
    140             "span.ins {" +
    141             "       background-color: #88ff88; }" +
    142             "table.summary {" +
    143             "       border: 1px solid black;" +
    144             "       margin-top: 20px;}" +
    145             "table.summary td {" +
    146             "       padding: 3px;}" +
    147             "span.listItem {" +
    148             "       font-size: 11px;" +
    149             "       font-weight: normal;" +
    150             "       text-transform: uppercase;" +
    151             "       padding: 3px;" +
    152             "       -webkit-border-radius: 4px;}" +
    153             "span." + AbstractResult.ResultCode.RESULTS_DIFFER.name() + "{" +
    154             "       background-color: #ccc;" +
    155             "       color: black;}" +
    156             "span." + AbstractResult.ResultCode.NO_EXPECTED_RESULT.name() + "{" +
    157             "       background-color: #a700e4;" +
    158             "       color: #fff;}" +
    159             "span.timed_out {" +
    160             "       background-color: #f3cb00;" +
    161             "       color: black;}" +
    162             "span.crashed {" +
    163             "       background-color: #c30000;" +
    164             "       color: #fff;}" +
    165             "span.noLtc {" +
    166             "       background-color: #944000;" +
    167             "       color: #fff;}" +
    168             "span.noEventSender {" +
    169             "       background-color: #815600;" +
    170             "       color: #fff;}" +
    171             "</style>";
    172 
    173     private static final String SCRIPT =
    174             "<script type=\"text/javascript\">" +
    175             "    function toggleDisplay(id) {" +
    176             "        element = document.getElementById(id);" +
    177             "        triangle = document.getElementById('tri.' + id);" +
    178             "        if (element.style.display == 'none') {" +
    179             "            element.style.display = 'inline';" +
    180             "            triangle.innerHTML = '&#x25bc; ';" +
    181             "        } else {" +
    182             "            element.style.display = 'none';" +
    183             "            triangle.innerHTML = '&#x25b6; ';" +
    184             "        }" +
    185             "    }" +
    186             "</script>";
    187 
    188     /** TODO: Make it a setting */
    189     private static final String HTML_DETAILS_RELATIVE_PATH = "details.html";
    190     private static final String TXT_SUMMARY_RELATIVE_PATH = "summary.txt";
    191 
    192     private static final int RESULTS_PER_DUMP = 500;
    193     private static final int RESULTS_PER_DB_ACCESS = 50;
    194 
    195     private int mCrashedTestsCount = 0;
    196     private List<AbstractResult> mUnexpectedFailures = new ArrayList<AbstractResult>();
    197     private List<AbstractResult> mExpectedFailures = new ArrayList<AbstractResult>();
    198     private List<AbstractResult> mExpectedPasses = new ArrayList<AbstractResult>();
    199     private List<AbstractResult> mUnexpectedPasses = new ArrayList<AbstractResult>();
    200 
    201     private Cursor mUnexpectedFailuresCursor;
    202     private Cursor mExpectedFailuresCursor;
    203     private Cursor mUnexpectedPassesCursor;
    204     private Cursor mExpectedPassesCursor;
    205 
    206     private FileFilter mFileFilter;
    207     private String mResultsRootDirPath;
    208     private String mTestsRelativePath;
    209     private Date mDate;
    210 
    211     private int mResultsSinceLastHtmlDump = 0;
    212     private int mResultsSinceLastDbAccess = 0;
    213 
    214     private SummarizerDBHelper mDbHelper;
    215 
    216     public Summarizer(String resultsRootDirPath, Context context) {
    217         mFileFilter = new FileFilter();
    218         mResultsRootDirPath = resultsRootDirPath;
    219 
    220         /**
    221          * We don't run the database I/O in a separate thread to avoid consumer/producer problem
    222          * and to simplify code.
    223          */
    224         mDbHelper = new SummarizerDBHelper(context);
    225         mDbHelper.open();
    226     }
    227 
    228     public static URI getDetailsUri() {
    229         return new File(ManagerService.RESULTS_ROOT_DIR_PATH + File.separator +
    230                 HTML_DETAILS_RELATIVE_PATH).toURI();
    231     }
    232 
    233     public void appendTest(AbstractResult result) {
    234         String relativePath = result.getRelativePath();
    235 
    236         if (result.didCrash()) {
    237             mCrashedTestsCount++;
    238         }
    239 
    240         if (result.didPass()) {
    241             result.clearResults();
    242             if (mFileFilter.isFail(relativePath)) {
    243                 mUnexpectedPasses.add(result);
    244             } else {
    245                 mExpectedPasses.add(result);
    246             }
    247         } else {
    248             if (mFileFilter.isFail(relativePath)) {
    249                 mExpectedFailures.add(result);
    250             } else {
    251                 mUnexpectedFailures.add(result);
    252             }
    253         }
    254 
    255         if (++mResultsSinceLastDbAccess == RESULTS_PER_DB_ACCESS) {
    256             persistLists();
    257             clearLists();
    258         }
    259     }
    260 
    261     private void clearLists() {
    262         mUnexpectedFailures.clear();
    263         mExpectedFailures.clear();
    264         mUnexpectedPasses.clear();
    265         mExpectedPasses.clear();
    266     }
    267 
    268     private void persistLists() {
    269         persistListToTable(mUnexpectedFailures, SummarizerDBHelper.UNEXPECTED_FAILURES_TABLE);
    270         persistListToTable(mExpectedFailures, SummarizerDBHelper.EXPECTED_FAILURES_TABLE);
    271         persistListToTable(mUnexpectedPasses, SummarizerDBHelper.UNEXPECTED_PASSES_TABLE);
    272         persistListToTable(mExpectedPasses, SummarizerDBHelper.EXPECTED_PASSES_TABLE);
    273         mResultsSinceLastDbAccess = 0;
    274     }
    275 
    276     private void persistListToTable(List<AbstractResult> results, String table) {
    277         for (AbstractResult abstractResult : results) {
    278             mDbHelper.insertAbstractResult(abstractResult, table);
    279         }
    280     }
    281 
    282     public void setTestsRelativePath(String testsRelativePath) {
    283         mTestsRelativePath = testsRelativePath;
    284     }
    285 
    286     public void summarize(Message onFinishMessage) {
    287         persistLists();
    288         clearLists();
    289 
    290         mUnexpectedFailuresCursor =
    291             mDbHelper.getAbstractResults(SummarizerDBHelper.UNEXPECTED_FAILURES_TABLE);
    292         mUnexpectedPassesCursor =
    293             mDbHelper.getAbstractResults(SummarizerDBHelper.UNEXPECTED_PASSES_TABLE);
    294         mExpectedFailuresCursor =
    295             mDbHelper.getAbstractResults(SummarizerDBHelper.EXPECTED_FAILURES_TABLE);
    296         mExpectedPassesCursor =
    297             mDbHelper.getAbstractResults(SummarizerDBHelper.EXPECTED_PASSES_TABLE);
    298 
    299         String webKitRevision = getWebKitRevision();
    300         createHtmlDetails(webKitRevision);
    301         createTxtSummary(webKitRevision);
    302 
    303         clearLists();
    304         mUnexpectedFailuresCursor.close();
    305         mUnexpectedPassesCursor.close();
    306         mExpectedFailuresCursor.close();
    307         mExpectedPassesCursor.close();
    308 
    309         onFinishMessage.sendToTarget();
    310     }
    311 
    312     public void reset() {
    313         mCrashedTestsCount = 0;
    314         clearLists();
    315         mDbHelper.reset();
    316         mDate = new Date();
    317     }
    318 
    319     private void dumpHtmlToFile(StringBuilder html, boolean append) {
    320         FsUtils.writeDataToStorage(new File(mResultsRootDirPath, HTML_DETAILS_RELATIVE_PATH),
    321                 html.toString().getBytes(), append);
    322         html.setLength(0);
    323         mResultsSinceLastHtmlDump = 0;
    324     }
    325 
    326     private void createTxtSummary(String webKitRevision) {
    327         StringBuilder txt = new StringBuilder();
    328 
    329         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    330         txt.append("Path: " + mTestsRelativePath + "\n");
    331         txt.append("Date: " + dateFormat.format(mDate) + "\n");
    332         txt.append("Build fingerprint: " + Build.FINGERPRINT + "\n");
    333         txt.append("WebKit version: " + getWebKitVersionFromUserAgentString() + "\n");
    334         txt.append("WebKit revision: " + webKitRevision + "\n");
    335 
    336         txt.append("TOTAL:                     " + getTotalTestCount() + "\n");
    337         txt.append("CRASHED (among all tests): " + mCrashedTestsCount + "\n");
    338         txt.append("UNEXPECTED FAILURES:       " + mUnexpectedFailuresCursor.getCount() + "\n");
    339         txt.append("UNEXPECTED PASSES:         " + mUnexpectedPassesCursor.getCount() + "\n");
    340         txt.append("EXPECTED FAILURES:         " + mExpectedFailuresCursor.getCount() + "\n");
    341         txt.append("EXPECTED PASSES:           " + mExpectedPassesCursor.getCount() + "\n");
    342 
    343         FsUtils.writeDataToStorage(new File(mResultsRootDirPath, TXT_SUMMARY_RELATIVE_PATH),
    344                 txt.toString().getBytes(), false);
    345     }
    346 
    347     private void createHtmlDetails(String webKitRevision) {
    348         StringBuilder html = new StringBuilder();
    349 
    350         html.append("<html><head>");
    351         html.append(CSS);
    352         html.append(SCRIPT);
    353         html.append("</head><body>");
    354 
    355         createTopSummaryTable(webKitRevision, html);
    356         dumpHtmlToFile(html, false);
    357 
    358         createResultsList(html, "Unexpected failures", mUnexpectedFailuresCursor);
    359         createResultsList(html, "Unexpected passes", mUnexpectedPassesCursor);
    360         createResultsList(html, "Expected failures", mExpectedFailuresCursor);
    361         createResultsList(html, "Expected passes", mExpectedPassesCursor);
    362 
    363         html.append("</body></html>");
    364         dumpHtmlToFile(html, true);
    365     }
    366 
    367     private int getTotalTestCount() {
    368         return mUnexpectedFailuresCursor.getCount() +
    369                 mUnexpectedPassesCursor.getCount() +
    370                 mExpectedPassesCursor.getCount() +
    371                 mExpectedFailuresCursor.getCount();
    372     }
    373 
    374     private String getWebKitVersionFromUserAgentString() {
    375         Resources resources = new Resources(new AssetManager(), new DisplayMetrics(),
    376                 new Configuration());
    377         String userAgent =
    378                 resources.getString(com.android.internal.R.string.web_user_agent);
    379 
    380         Matcher matcher = Pattern.compile("AppleWebKit/([0-9]+?\\.[0-9])").matcher(userAgent);
    381         if (matcher.find()) {
    382             return matcher.group(1);
    383         }
    384         return "unknown";
    385     }
    386 
    387     private String getWebKitRevision() {
    388         URL url = null;
    389         try {
    390             url = new URL(ForwarderManager.getHostSchemePort(false) + "ThirdPartyProject.prop");
    391         } catch (MalformedURLException e) {
    392             assert false;
    393         }
    394 
    395         String thirdPartyProjectContents = new String(FsUtils.readDataFromUrl(url));
    396         Matcher matcher = Pattern.compile("^version=([0-9]+)", Pattern.MULTILINE).matcher(
    397                 thirdPartyProjectContents);
    398         if (matcher.find()) {
    399             return matcher.group(1);
    400         }
    401         return "unknown";
    402     }
    403 
    404     private void createTopSummaryTable(String webKitRevision, StringBuilder html) {
    405         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    406         html.append("<h1>" + "Layout tests' results for: " +
    407                 (mTestsRelativePath.equals("") ? "all tests" : mTestsRelativePath) + "</h1>");
    408         html.append("<h3>" + "Date: " + dateFormat.format(new Date()) + "</h3>");
    409         html.append("<h3>" + "Build fingerprint: " + Build.FINGERPRINT + "</h3>");
    410         html.append("<h3>" + "WebKit version: " + getWebKitVersionFromUserAgentString() + "</h3>");
    411 
    412         html.append("<h3>" + "WebKit revision: ");
    413         html.append("<a href=\"http://trac.webkit.org/browser/trunk?rev=" + webKitRevision +
    414                 "\" target=\"_blank\"><span class=\"path\">" + webKitRevision + "</span></a>");
    415         html.append("</h3>");
    416 
    417         html.append("<table class=\"summary\">");
    418         createSummaryTableRow(html, "TOTAL", getTotalTestCount());
    419         createSummaryTableRow(html, "CRASHED (among all tests)", mCrashedTestsCount);
    420         createSummaryTableRow(html, "UNEXPECTED FAILURES", mUnexpectedFailuresCursor.getCount());
    421         createSummaryTableRow(html, "UNEXPECTED PASSES", mUnexpectedPassesCursor.getCount());
    422         createSummaryTableRow(html, "EXPECTED FAILURES", mExpectedFailuresCursor.getCount());
    423         createSummaryTableRow(html, "EXPECTED PASSES", mExpectedPassesCursor.getCount());
    424         html.append("</table>");
    425     }
    426 
    427     private void createSummaryTableRow(StringBuilder html, String caption, int size) {
    428         html.append("<tr>");
    429         html.append("    <td>" + caption + "</td>");
    430         html.append("    <td>" + size + "</td>");
    431         html.append("</tr>");
    432     }
    433 
    434     private void createResultsList(
    435             StringBuilder html, String title, Cursor cursor) {
    436         String relativePath;
    437         String id = "";
    438         AbstractResult.ResultCode resultCode;
    439 
    440         html.append("<h2>" + title + " [" + cursor.getCount() + "]</h2>");
    441 
    442         if (!cursor.moveToFirst()) {
    443             return;
    444         }
    445 
    446         AbstractResult result;
    447         do {
    448             result = SummarizerDBHelper.getAbstractResult(cursor);
    449 
    450             relativePath = result.getRelativePath();
    451             resultCode = result.getResultCode();
    452 
    453             html.append("<h3>");
    454 
    455             /**
    456              * Technically, two different paths could end up being the same, because
    457              * ':' is a valid  character in a path. However, it is probably not going
    458              * to cause any problems in this case
    459              */
    460             id = relativePath.replace(File.separator, ":");
    461 
    462             /** Write the test name */
    463             if (resultCode == AbstractResult.ResultCode.RESULTS_DIFFER) {
    464                 html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');");
    465                 html.append("return false;\">");
    466                 html.append("<span class=\"tri\" id=\"tri." + id + "\">&#x25b6; </span>");
    467                 html.append("<span class=\"path\">" + relativePath + "</span>");
    468                 html.append("</a>");
    469             } else {
    470                 html.append("<a href=\"" + getViewSourceUrl(result.getRelativePath()).toString() + "\"");
    471                 html.append(" target=\"_blank\">");
    472                 html.append("<span class=\"sqr sqr_" + (result.didPass() ? "pass" : "fail"));
    473                 html.append("\">&#x25a0; </span>");
    474                 html.append("<span class=\"path\">" + result.getRelativePath() + "</span>");
    475                 html.append("</a>");
    476             }
    477 
    478             if (!result.didPass()) {
    479                 appendTags(html, result);
    480             }
    481 
    482             html.append("</h3>");
    483             appendExpectedResultsSources(result, html);
    484 
    485             if (resultCode == AbstractResult.ResultCode.RESULTS_DIFFER) {
    486                 html.append("<div class=\"diff\" style=\"display: none;\" id=\"" + id + "\">");
    487                 html.append(result.getDiffAsHtml());
    488                 html.append("<a href=\"#\" onClick=\"toggleDisplay('" + id + "');");
    489                 html.append("return false;\">Hide</a>");
    490                 html.append(" | ");
    491                 html.append("<a href=\"" + getViewSourceUrl(relativePath).toString() + "\"");
    492                 html.append(" target=\"_blank\">Show source</a>");
    493                 html.append("</div>");
    494             }
    495 
    496             html.append("<div class=\"space\"></div>");
    497 
    498             if (++mResultsSinceLastHtmlDump == RESULTS_PER_DUMP) {
    499                 dumpHtmlToFile(html, true);
    500             }
    501 
    502             cursor.moveToNext();
    503         } while (!cursor.isAfterLast());
    504     }
    505 
    506     private void appendTags(StringBuilder html, AbstractResult result) {
    507         /** Tag tests which crash, time out or where results don't match */
    508         if (result.didCrash()) {
    509             html.append(" <span class=\"listItem crashed\">Crashed</span>");
    510         } else {
    511             if (result.didTimeOut()) {
    512                 html.append(" <span class=\"listItem timed_out\">Timed out</span>");
    513             }
    514             AbstractResult.ResultCode resultCode = result.getResultCode();
    515             if (resultCode != AbstractResult.ResultCode.RESULTS_MATCH) {
    516                 html.append(" <span class=\"listItem " + resultCode.name() + "\">");
    517                 html.append(resultCode.toString());
    518                 html.append("</span>");
    519             }
    520         }
    521 
    522         /** Detect missing LTC function */
    523         String additionalTextOutputString = result.getAdditionalTextOutputString();
    524         if (additionalTextOutputString != null &&
    525                 additionalTextOutputString.contains("com.android.dumprendertree") &&
    526                 additionalTextOutputString.contains("has no method")) {
    527             if (additionalTextOutputString.contains("LayoutTestController")) {
    528                 html.append(" <span class=\"listItem noLtc\">LTC function missing</span>");
    529             }
    530             if (additionalTextOutputString.contains("EventSender")) {
    531                 html.append(" <span class=\"listItem noEventSender\">");
    532                 html.append("ES function missing</span>");
    533             }
    534         }
    535     }
    536 
    537     private static final void appendExpectedResultsSources(AbstractResult result,
    538             StringBuilder html) {
    539         String textSource = result.getExpectedTextResultPath();
    540         String imageSource = result.getExpectedImageResultPath();
    541 
    542         if (result.didCrash()) {
    543             html.append("<span class=\"source\">Did not look for expected results</span>");
    544             return;
    545         }
    546 
    547         if (textSource == null) {
    548             // Show if a text result is missing. We may want to revisit this decision when we add
    549             // support for image results.
    550             html.append("<span class=\"source\">Expected textual result missing</span>");
    551         } else {
    552             html.append("<span class=\"source\">Expected textual result from: ");
    553             html.append("<a href=\"" + ForwarderManager.getHostSchemePort(false) + "LayoutTests/" +
    554                     textSource + "\"");
    555             html.append(" target=\"_blank\">");
    556             html.append(textSource + "</a></span>");
    557         }
    558         if (imageSource != null) {
    559             html.append("<span class=\"source\">Expected image result from: ");
    560             html.append("<a href=\"" + ForwarderManager.getHostSchemePort(false) + "LayoutTests/" +
    561                     imageSource + "\"");
    562             html.append(" target=\"_blank\">");
    563             html.append(imageSource + "</a></span>");
    564         }
    565     }
    566 
    567     private static final URL getViewSourceUrl(String relativePath) {
    568         URL url = null;
    569         try {
    570             url = new URL("http", "localhost", ForwarderManager.HTTP_PORT,
    571                     "/Tools/DumpRenderTree/android/view_source.php?src=" +
    572                     relativePath);
    573         } catch (MalformedURLException e) {
    574             assert false : "relativePath=" + relativePath;
    575         }
    576         return url;
    577     }
    578 }
    579