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