| # 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) |