# Copyright 2017 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ This is a utility to build an html page based on the directory summaries collected during the test. """ import os import re import common from autotest_lib.client.bin.result_tools import utils_lib from autotest_lib.client.common_lib import global_config CONFIG = global_config.global_config # Base url to open a file from Google Storage GS_FILE_BASE_URL = CONFIG.get_config_value('CROS', 'gs_file_base_url') # Default width of `size_trimmed_width`. If throttle is not applied, the block # of `size_trimmed_width` will be set to minimum to make the view more compact. DEFAULT_SIZE_TRIMMED_WIDTH = 50 DEFAULT_RESULT_SUMMARY_NAME = 'result_summary.html' DIR_SUMMARY_PATTERN = 'dir_summary_\d+.json' # ================================================== # Following are key names used in the html templates: CSS = 'css' DIRS = 'dirs' GS_FILE_BASE_URL_KEY = 'gs_file_base_url' INDENTATION_KEY = 'indentation' JAVASCRIPT = 'javascript' JOB_DIR = 'job_dir' NAME = 'name' PATH = 'path' SIZE_CLIENT_COLLECTED = 'size_client_collected' SIZE_INFO = 'size_info' SIZE_ORIGINAL = 'size_original' SIZE_PERCENT = 'size_percent' SIZE_PERCENT_CLASS = 'size_percent_class' SIZE_PERCENT_CLASS_REGULAR = 'size_percent' SIZE_PERCENT_CLASS_TOP = 'top_size_percent' SIZE_SUMMARY = 'size_summary' SIZE_TRIMMED = 'size_trimmed' # Width of `size_trimmed` block` SIZE_TRIMMED_WIDTH = 'size_trimmed_width' SUBDIRS = 'subdirs' SUMMARY_TREE = 'summary_tree' # ================================================== # Text to show when test result is not throttled. NOT_THROTTLED = '(Not throttled)' PAGE_TEMPLATE = """ <!DOCTYPE html> <html> <body onload="init()"> <h3>Summary of test results</h3> %(size_summary)s <p> <b> Display format of a file or directory: </b> </p> <p> <span class="size_percent" style="width:auto"> [percentage of size in the parent directory] </span> <span class="size_original" style="width:auto"> [original size] </span> <span class="size_trimmed" style="width:auto"> [size after throttling (empty if not throttled)] </span> [file name (<strike>strikethrough</strike> if file was deleted due to throttling)] </p> <button onclick="expandAll();">Expand All</button> <button onclick="collapseAll();">Collapse All</button> %(summary_tree)s %(css)s %(javascript)s </body> </html> """ CSS_TEMPLATE = """ <style> body { font-family: Arial; } td.table_header { font-weight: normal; } span.size_percent { color: #e8773e; display: inline-block; font-size: 75%%; text-align: right; width: 35px; } span.top_size_percent { color: #e8773e; background-color: yellow; display: inline-block; font-size: 75%%; fount-weight: bold; text-align: right; width: 35px; } span.size_original { color: sienna; display: inline-block; font-size: 75%%; text-align: right; width: 50px; } span.size_trimmed { color: green; display: inline-block; font-size: 75%%; text-align: right; width: %(size_trimmed_width)dpx; } ul.tree li { list-style-type: none; position: relative; } ul.tree li ul { display: none; } ul.tree li.open > ul { display: block; } ul.tree li a { color: black; text-decoration: none; } ul.tree li a.file { color: blue; text-decoration: underline; } ul.tree li a:before { height: 1em; padding:0 .1em; font-size: .8em; display: block; position: absolute; left: -1.3em; top: .2em; } ul.tree li > a:not(:last-child):before { content: '+'; } ul.tree li.open > a:not(:last-child):before { content: '-'; } </style> """ JAVASCRIPT_TEMPLATE = """ <script> function init() { var tree = document.querySelectorAll('ul.tree a:not(:last-child)'); for(var i = 0; i < tree.length; i++){ tree[i].addEventListener('click', function(e) { var parent = e.target.parentElement; var classList = parent.classList; if(classList.contains("open")) { classList.remove('open'); var opensubs = parent.querySelectorAll(':scope .open'); for(var i = 0; i < opensubs.length; i++){ opensubs[i].classList.remove('open'); } } else { classList.add('open'); } }); } } function expandAll() { var tree = document.querySelectorAll('ul.tree a:not(:last-child)'); for(var i = 0; i < tree.length; i++){ var classList = tree[i].parentElement.classList; if(classList.contains("close")) { classList.remove('close'); } classList.add('open'); } } function collapseAll() { var tree = document.querySelectorAll('ul.tree a:not(:last-child)'); for(var i = 0; i < tree.length; i++){ var classList = tree[i].parentElement.classList; if(classList.contains("open")) { classList.remove('open'); } classList.add('close'); } } // If the current url has `gs_url`, it means the file is opened from Google // Storage. var gs_url = 'apidata.googleusercontent.com'; // Base url to open a file from Google Storage var gs_file_base_url = '%(gs_file_base_url)s' // Path to the result. var job_dir = '%(job_dir)s' function openFile(path) { if(window.location.href.includes(gs_url)) { url = gs_file_base_url + job_dir + '/' + path.substring(3); } else { url = window.location.href + '/' + path; } window.open(url, '_blank'); } </script> """ SIZE_SUMMARY_TEMPLATE = """ <table> <tr> <td class="table_header">Results collected from test device: </td> <td><span>%(size_client_collected)s</span> </td> </tr> <tr> <td class="table_header">Original size of test results:</td> <td> <span class="size_original" style="font-size:100%%;width:auto"> %(size_original)s </span> </td> </tr> <tr> <td class="table_header">Size of test results after throttling:</td> <td> <span class="size_trimmed" style="font-size:100%%;width:auto"> %(size_trimmed)s </span> </td> </tr> </table> """ SIZE_INFO_TEMPLATE = """ %(indentation)s<span class="%(size_percent_class)s">%(size_percent)s</span> %(indentation)s<span class="size_original">%(size_original)s</span> %(indentation)s<span class="size_trimmed">%(size_trimmed)s</span> """ FILE_ENTRY_TEMPLATE = """ %(indentation)s<li> %(indentation)s\t<div> %(size_info)s %(indentation)s\t\t<a class="file" href="javascript:openFile('%(path)s');" > %(indentation)s\t\t\t%(name)s %(indentation)s\t\t</a> %(indentation)s\t</div> %(indentation)s</li>""" DELETED_FILE_ENTRY_TEMPLATE = """ %(indentation)s<li> %(indentation)s\t<div> %(size_info)s %(indentation)s\t\t<strike>%(name)s</strike> %(indentation)s\t</div> %(indentation)s</li>""" DIR_ENTRY_TEMPLATE = """ %(indentation)s<li><a>%(size_info)s %(name)s</a> %(subdirs)s %(indentation)s</li>""" SUBDIRS_WRAPPER_TEMPLATE = """ %(indentation)s<ul class="tree"> %(dirs)s %(indentation)s</ul>""" INDENTATION = '\t' def _get_size_percent(size_original, total_bytes): """Get the percentage of file size in the parent directory before throttled. @param size_original: Original size of the file, in bytes. @param total_bytes: Total size of all files under the parent directory, in bytes. @return: A formatted string of the percentage of file size in the parent directory before throttled. """ if total_bytes == 0: return '0%' return '%.1f%%' % (100*float(size_original)/total_bytes) def _get_dirs_html(dirs, parent_path, total_bytes, indentation): """Get the html string for the given directory. @param dirs: A list of ResultInfo. @param parent_path: Path to the parent directory. @param total_bytes: Total of the original size of files in the given directories in bytes. @param indentation: Indentation to be used for the html. """ if not dirs: return '' summary_html = '' top_size_limit = max([entry.original_size for entry in dirs]) # A map between file name to ResultInfo that contains the summary of the # file. entries = dict((entry.keys()[0], entry) for entry in dirs) for name in sorted(entries.keys()): entry = entries[name] if not entry.is_dir and re.match(DIR_SUMMARY_PATTERN, name): # Do not include directory summary json files in the html, as they # will be deleted. continue size_data = {SIZE_PERCENT: _get_size_percent(entry.original_size, total_bytes), SIZE_ORIGINAL: utils_lib.get_size_string(entry.original_size), SIZE_TRIMMED: utils_lib.get_size_string(entry.trimmed_size), INDENTATION_KEY: indentation + 2*INDENTATION} if entry.original_size < top_size_limit: size_data[SIZE_PERCENT_CLASS] = SIZE_PERCENT_CLASS_REGULAR else: size_data[SIZE_PERCENT_CLASS] = SIZE_PERCENT_CLASS_TOP if entry.trimmed_size == entry.original_size: size_data[SIZE_TRIMMED] = '' entry_path = '%s/%s' % (parent_path, name) if not entry.is_dir: # This is a file data = {NAME: name, PATH: entry_path, SIZE_INFO: SIZE_INFO_TEMPLATE % size_data, INDENTATION_KEY: indentation} if entry.original_size > 0 and entry.trimmed_size == 0: summary_html += DELETED_FILE_ENTRY_TEMPLATE % data else: summary_html += FILE_ENTRY_TEMPLATE % data else: subdir_total_size = entry.original_size sub_indentation = indentation + INDENTATION subdirs_html = ( SUBDIRS_WRAPPER_TEMPLATE % {DIRS: _get_dirs_html( entry.files, entry_path, subdir_total_size, sub_indentation), INDENTATION_KEY: indentation}) data = {NAME: entry.name, SIZE_INFO: SIZE_INFO_TEMPLATE % size_data, SUBDIRS: subdirs_html, INDENTATION_KEY: indentation} summary_html += DIR_ENTRY_TEMPLATE % data return summary_html def build(client_collected_bytes, summary, html_file): """Generate an HTML file to visualize the given directory summary. @param client_collected_bytes: The total size of results collected from the DUT. The number can be larger than the total file size of the given path, as files can be overwritten or removed. @param summary: A ResultInfo instance containing the directory summary. @param html_file: Path to save the html file to. """ size_original = summary.original_size size_trimmed = summary.trimmed_size size_summary_data = {SIZE_CLIENT_COLLECTED: utils_lib.get_size_string(client_collected_bytes), SIZE_ORIGINAL: utils_lib.get_size_string(size_original), SIZE_TRIMMED: utils_lib.get_size_string(size_trimmed)} size_trimmed_width = DEFAULT_SIZE_TRIMMED_WIDTH if size_original == size_trimmed: size_summary_data[SIZE_TRIMMED] = NOT_THROTTLED size_trimmed_width = 0 size_summary = SIZE_SUMMARY_TEMPLATE % size_summary_data indentation = INDENTATION dirs_html = _get_dirs_html( summary.files, '..', size_original, indentation + INDENTATION) summary_tree = SUBDIRS_WRAPPER_TEMPLATE % {DIRS: dirs_html, INDENTATION_KEY: indentation} # job_dir is the path between Autotest `results` folder and the summary html # file, e.g., 123-debug_user/host1. Assume it always contains 2 levels. job_dir_sections = html_file.split(os.sep)[:-1] try: job_dir = '/'.join(job_dir_sections[ (job_dir_sections.index('results')+1):]) except ValueError: # 'results' is not in the path, default to two levels up of the summary # file. job_dir = '/'.join(job_dir_sections[-2:]) javascript = (JAVASCRIPT_TEMPLATE % {GS_FILE_BASE_URL_KEY: GS_FILE_BASE_URL, JOB_DIR: job_dir}) css = CSS_TEMPLATE % {SIZE_TRIMMED_WIDTH: size_trimmed_width} html = PAGE_TEMPLATE % {SIZE_SUMMARY: size_summary, SUMMARY_TREE: summary_tree, CSS: css, JAVASCRIPT: javascript} with open(html_file, 'w') as f: f.write(html)