1# Copyright 2017 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6This is a utility to build an html page based on the directory summaries 7collected during the test. 8""" 9 10import math 11import os 12 13import common 14from autotest_lib.client.bin.result_tools import utils_lib 15from autotest_lib.client.common_lib import global_config 16 17 18CONFIG = global_config.global_config 19# Base url to open a file from Google Storage 20GS_FILE_BASE_URL = CONFIG.get_config_value('CROS', 'gs_file_base_url') 21 22# Default width of `size_trimmed_width`. If throttle is not applied, the block 23# of `size_trimmed_width` will be set to minimum to make the view more compact. 24DEFAULT_SIZE_TRIMMED_WIDTH = 50 25 26DEFAULT_RESULT_SUMMARY_NAME = 'result_summary.html' 27 28# ================================================== 29# Following are key names used in the html templates: 30 31CSS = 'css' 32DIRS = 'dirs' 33GS_FILE_BASE_URL_KEY = 'gs_file_base_url' 34INDENTATION_KEY = 'indentation' 35JAVASCRIPT = 'javascript' 36JOB_DIR = 'job_dir' 37NAME = 'name' 38PATH = 'path' 39 40SIZE_CLIENT_COLLECTED = 'size_client_collected' 41 42SIZE_INFO = 'size_info' 43SIZE_ORIGINAL = 'size_original' 44SIZE_PERCENT = 'size_percent' 45SIZE_PERCENT_CLASS = 'size_percent_class' 46SIZE_PERCENT_CLASS_REGULAR = 'size_percent' 47SIZE_PERCENT_CLASS_TOP = 'top_size_percent' 48SIZE_SUMMARY = 'size_summary' 49SIZE_TRIMMED = 'size_trimmed' 50 51# Width of `size_trimmed` block` 52SIZE_TRIMMED_WIDTH = 'size_trimmed_width' 53 54SUBDIRS = 'subdirs' 55SUMMARY_TREE = 'summary_tree' 56# ================================================== 57 58# Text to show when test result is not throttled. 59NOT_THROTTLED = '(Not throttled)' 60 61 62PAGE_TEMPLATE = """ 63<!DOCTYPE html> 64 <html> 65 <body onload="init()"> 66 <h3>Summary of test results</h3> 67%(size_summary)s 68 <p> 69 <b> 70 Display format of a file or directory: 71 </b> 72 </p> 73 <p> 74 <span class="size_percent" style="width:auto"> 75 [percentage of size in the parent directory] 76 </span> 77 <span class="size_original" style="width:auto"> 78 [original size] 79 </span> 80 <span class="size_trimmed" style="width:auto"> 81 [size after throttling (empty if not throttled)] 82 </span> 83 [file name] 84 </p> 85 86 <button onclick="expandAll();">Expand All</button> 87 <button onclick="collapseAll();">Collapse All</button> 88 89%(summary_tree)s 90 91%(css)s 92%(javascript)s 93 94 </body> 95</html> 96""" 97 98CSS_TEMPLATE = """ 99<style> 100 body { 101 font-family: Arial; 102 } 103 104 td.table_header { 105 font-weight: normal; 106 } 107 108 span.size_percent { 109 color: #e8773e; 110 display: inline-block; 111 font-size: 75%%; 112 text-align: right; 113 width: 35px; 114 } 115 116 span.top_size_percent { 117 color: #e8773e; 118 background-color: yellow; 119 display: inline-block; 120 font-size: 75%%; 121 fount-weight: bold; 122 text-align: right; 123 width: 35px; 124 } 125 126 span.size_original { 127 color: sienna; 128 display: inline-block; 129 font-size: 75%%; 130 text-align: right; 131 width: 50px; 132 } 133 134 span.size_trimmed { 135 color: green; 136 display: inline-block; 137 font-size: 75%%; 138 text-align: right; 139 width: %(size_trimmed_width)dpx; 140 } 141 142 ul.tree li { 143 list-style-type: none; 144 position: relative; 145 } 146 147 ul.tree li ul { 148 display: none; 149 } 150 151 ul.tree li.open > ul { 152 display: block; 153 } 154 155 ul.tree li a { 156 color: black; 157 text-decoration: none; 158 } 159 160 ul.tree li a.file { 161 color: blue; 162 text-decoration: underline; 163 } 164 165 ul.tree li a:before { 166 height: 1em; 167 padding:0 .1em; 168 font-size: .8em; 169 display: block; 170 position: absolute; 171 left: -1.3em; 172 top: .2em; 173 } 174 175 ul.tree li > a:not(:last-child):before { 176 content: '+'; 177 } 178 179 ul.tree li.open > a:not(:last-child):before { 180 content: '-'; 181 } 182</style> 183""" 184 185JAVASCRIPT_TEMPLATE = """ 186<script> 187function init() { 188 var tree = document.querySelectorAll('ul.tree a:not(:last-child)'); 189 for(var i = 0; i < tree.length; i++){ 190 tree[i].addEventListener('click', function(e) { 191 var parent = e.target.parentElement; 192 var classList = parent.classList; 193 if(classList.contains("open")) { 194 classList.remove('open'); 195 var opensubs = parent.querySelectorAll(':scope .open'); 196 for(var i = 0; i < opensubs.length; i++){ 197 opensubs[i].classList.remove('open'); 198 } 199 } else { 200 classList.add('open'); 201 } 202 }); 203 } 204} 205 206function expandAll() { 207 var tree = document.querySelectorAll('ul.tree a:not(:last-child)'); 208 for(var i = 0; i < tree.length; i++){ 209 var classList = tree[i].parentElement.classList; 210 if(classList.contains("close")) { 211 classList.remove('close'); 212 } 213 classList.add('open'); 214 } 215} 216 217function collapseAll() { 218 var tree = document.querySelectorAll('ul.tree a:not(:last-child)'); 219 for(var i = 0; i < tree.length; i++){ 220 var classList = tree[i].parentElement.classList; 221 if(classList.contains("open")) { 222 classList.remove('open'); 223 } 224 classList.add('close'); 225 } 226} 227 228// If the current url has `gs_url`, it means the file is opened from Google 229// Storage. 230var gs_url = 'apidata.googleusercontent.com'; 231// Base url to open a file from Google Storage 232var gs_file_base_url = '%(gs_file_base_url)s' 233// Path to the result. 234var job_dir = '%(job_dir)s' 235 236function openFile(path) { 237 if(window.location.href.includes(gs_url)) { 238 url = gs_file_base_url + job_dir + '/' + path.substring(3); 239 } else { 240 url = window.location.href + '/' + path; 241 } 242 window.open(url, '_blank'); 243} 244</script> 245""" 246 247SIZE_SUMMARY_TEMPLATE = """ 248<table> 249 <tr> 250 <td class="table_header">Results collected from test device: </td> 251 <td><span>%(size_client_collected)s</span> </td> 252 </tr> 253 <tr> 254 <td class="table_header">Original size of test results:</td> 255 <td> 256 <span class="size_original" style="font-size:100%%;width:auto"> 257 %(size_original)s 258 </span> 259 </td> 260 </tr> 261 <tr> 262 <td class="table_header">Size of test results after throttling:</td> 263 <td> 264 <span class="size_trimmed" style="font-size:100%%;width:auto"> 265 %(size_trimmed)s 266 </span> 267 </td> 268 </tr> 269</table> 270""" 271 272SIZE_INFO_TEMPLATE = """ 273%(indentation)s<span class="%(size_percent_class)s">%(size_percent)s</span> 274%(indentation)s<span class="size_original">%(size_original)s</span> 275%(indentation)s<span class="size_trimmed">%(size_trimmed)s</span> """ 276 277FILE_ENTRY_TEMPLATE = """ 278%(indentation)s<li> 279%(indentation)s\t<div> 280%(size_info)s 281%(indentation)s\t\t<a class="file" href="javascript:openFile('%(path)s');" > 282%(indentation)s\t\t\t%(name)s 283%(indentation)s\t\t</a> 284%(indentation)s\t</div> 285%(indentation)s</li>""" 286 287DIR_ENTRY_TEMPLATE = """ 288%(indentation)s<li><a>%(size_info)s %(name)s</a> 289%(subdirs)s 290%(indentation)s</li>""" 291 292SUBDIRS_WRAPPER_TEMPLATE = """ 293%(indentation)s<ul class="tree"> 294%(dirs)s 295%(indentation)s</ul>""" 296 297INDENTATION = '\t' 298 299def _get_size_percent(size_original, total_bytes): 300 """Get the percentage of file size in the parent directory before throttled. 301 302 @param size_original: Original size of the file, in bytes. 303 @param total_bytes: Total size of all files under the parent directory, in 304 bytes. 305 @return: A formatted string of the percentage of file size in the parent 306 directory before throttled. 307 """ 308 if total_bytes == 0: 309 return '0%' 310 return '%.1f%%' % (100*float(size_original)/total_bytes) 311 312 313def _get_size_string(size_bytes): 314 """Get a string of the given bytes. 315 316 Convert the number of bytes to the closest integer of file size measure, 317 i.e., KB, MB etc. 318 319 @param size_bytes: Number of bytes. 320 @return: A string representing `size_bytes` in KB, MB etc. 321 """ 322 if size_bytes == 0: 323 return '0 B' 324 size_name = ('B', 'KB', 'MB', 'GB', 'TB', 'PB') 325 i = int(math.floor(math.log(size_bytes, 1024))) 326 p = math.pow(1024, i) 327 s = int(size_bytes / p) 328 return '%s %s' % (s, size_name[i]) 329 330 331def _get_dirs_html(dirs, parent_path, total_bytes, indentation): 332 """Get the html string for the given directory. 333 334 @param dirs: A dictionary of directory summary. 335 @param parent_path: Path to the parent directory. 336 @param total_bytes: Total of the original size of files in the given 337 directories in bytes. 338 @param indentation: Indentation to be used for the html. 339 """ 340 if not dirs: 341 return '' 342 summary_html = '' 343 top_size_limit = max([ 344 dirs[entry][utils_lib.ORIGINAL_SIZE_BYTES] for entry in dirs]) 345 for entry in sorted(dirs.keys()): 346 subdirs = dirs[entry].get(utils_lib.DIRS, {}) 347 348 size_original = dirs[entry][utils_lib.ORIGINAL_SIZE_BYTES] 349 size_trimmed = dirs[entry].get( 350 utils_lib.TRIMMED_SIZE_BYTES, size_original) 351 size_data = {SIZE_PERCENT: _get_size_percent(size_original, 352 total_bytes), 353 SIZE_ORIGINAL: _get_size_string(size_original), 354 SIZE_TRIMMED: _get_size_string(size_trimmed), 355 INDENTATION_KEY: indentation + 2*INDENTATION} 356 if size_original < top_size_limit: 357 size_data[SIZE_PERCENT_CLASS] = SIZE_PERCENT_CLASS_REGULAR 358 else: 359 size_data[SIZE_PERCENT_CLASS] = SIZE_PERCENT_CLASS_TOP 360 if size_trimmed == size_original: 361 size_data[SIZE_TRIMMED] = '' 362 363 entry_path = '%s/%s' % (parent_path, entry) 364 if not subdirs: 365 # This is a file 366 data = {NAME: entry, 367 PATH: entry_path, 368 SIZE_INFO: SIZE_INFO_TEMPLATE % size_data, 369 INDENTATION_KEY: indentation} 370 summary_html += FILE_ENTRY_TEMPLATE % data 371 else: 372 subdir_total_size = dirs[entry][utils_lib.ORIGINAL_SIZE_BYTES] 373 sub_indentation = indentation + INDENTATION 374 subdirs_html = ( 375 SUBDIRS_WRAPPER_TEMPLATE % 376 {DIRS: _get_dirs_html( 377 subdirs, entry_path, subdir_total_size, 378 sub_indentation), 379 INDENTATION_KEY: indentation}) 380 data = {NAME: entry, 381 SIZE_INFO: SIZE_INFO_TEMPLATE % size_data, 382 SUBDIRS: subdirs_html, 383 INDENTATION_KEY: indentation} 384 summary_html += DIR_ENTRY_TEMPLATE % data 385 return summary_html 386 387 388def build(client_collected_bytes, summary, html_file): 389 """Generate an HTML file to visualize the given directory summary. 390 391 @param client_collected_bytes: The total size of results collected from 392 the DUT. The number can be larger than the total file size of the 393 given path, as files can be overwritten or removed. 394 @param summary: A dictionary of result summary. 395 @param html_file: Path to save the html file to. 396 """ 397 dirs = summary[utils_lib.ROOT_DIR].get(utils_lib.DIRS, {}) 398 size_original = summary[utils_lib.ROOT_DIR][ 399 utils_lib.ORIGINAL_SIZE_BYTES] 400 size_trimmed = summary[utils_lib.ROOT_DIR].get( 401 utils_lib.TRIMMED_SIZE_BYTES, size_original) 402 size_summary_data = {SIZE_CLIENT_COLLECTED: 403 _get_size_string(client_collected_bytes), 404 SIZE_ORIGINAL: 405 _get_size_string(size_original), 406 SIZE_TRIMMED: 407 _get_size_string(size_trimmed)} 408 size_trimmed_width = DEFAULT_SIZE_TRIMMED_WIDTH 409 if size_original == size_trimmed: 410 size_summary_data[SIZE_TRIMMED] = NOT_THROTTLED 411 size_trimmed_width = 0 412 413 size_summary = SIZE_SUMMARY_TEMPLATE % size_summary_data 414 415 indentation = INDENTATION 416 dirs_html = _get_dirs_html( 417 dirs, '..', size_original, indentation + INDENTATION) 418 summary_tree = SUBDIRS_WRAPPER_TEMPLATE % {DIRS: dirs_html, 419 INDENTATION_KEY: indentation} 420 421 # job_dir is the path between Autotest `results` folder and the summary html 422 # file, e.g., 123-debug_user/host1. Assume it always contains 2 levels. 423 job_dir_sections = html_file.split(os.sep)[:-1] 424 try: 425 job_dir = '/'.join(job_dir_sections[ 426 (job_dir_sections.index('results')+1):]) 427 except ValueError: 428 # 'results' is not in the path, default to two levels up of the summary 429 # file. 430 job_dir = '/'.join(job_dir_sections[-2:]) 431 432 javascript = (JAVASCRIPT_TEMPLATE % 433 {GS_FILE_BASE_URL_KEY: GS_FILE_BASE_URL, 434 JOB_DIR: job_dir}) 435 css = CSS_TEMPLATE % {SIZE_TRIMMED_WIDTH: size_trimmed_width} 436 html = PAGE_TEMPLATE % {SIZE_SUMMARY: size_summary, 437 SUMMARY_TREE: summary_tree, 438 CSS: css, 439 JAVASCRIPT: javascript} 440 with open(html_file, 'w') as f: 441 f.write(html) 442