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 = '▼ ';" + 174 " } else {" + 175 " element.style.display = 'none';" + 176 " triangle.innerHTML = '▶ ';" + 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\">■ </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 + "\">▶ </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\">■ </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}