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