# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. # # Use of this source code is governed by a BSD-style license # that can be found in the LICENSE file in the root of the source # tree. An additional intellectual property rights grant can be found # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. import functools import hashlib import logging import os import re import sys try: import csscompressor except ImportError: logging.critical('Cannot import the third-party Python package csscompressor') sys.exit(1) try: import jsmin except ImportError: logging.critical('Cannot import the third-party Python package jsmin') sys.exit(1) class HtmlExport(object): """HTML exporter class for APM quality scores.""" _NEW_LINE = '\n' # CSS and JS file paths. _PATH = os.path.dirname(os.path.realpath(__file__)) _CSS_FILEPATH = os.path.join(_PATH, 'results.css') _CSS_MINIFIED = True _JS_FILEPATH = os.path.join(_PATH, 'results.js') _JS_MINIFIED = True def __init__(self, output_filepath): self._scores_data_frame = None self._output_filepath = output_filepath def Export(self, scores_data_frame): """Exports scores into an HTML file. Args: scores_data_frame: DataFrame instance. """ self._scores_data_frame = scores_data_frame html = ['', self._BuildHeader(), (''), '', self._BuildBody(), '', ''] self._Save(self._output_filepath, self._NEW_LINE.join(html)) def _BuildHeader(self): """Builds the section of the HTML file. The header contains the page title and either embedded or linked CSS and JS files. Returns: A string with ... HTML. """ html = ['', 'Results'] # Add Material Design hosted libs. html.append('') html.append('') html.append('') html.append('') # Embed custom JavaScript and CSS files. html.append('') html.append('') html.append('') return self._NEW_LINE.join(html) def _BuildBody(self): """Builds the content of the section.""" score_names = self._scores_data_frame['eval_score_name'].drop_duplicates( ).values.tolist() html = [ ('
'), '
', '
', 'APM QA results ({})'.format( self._output_filepath), '
', ] # Tab selectors. html.append('
') for tab_index, score_name in enumerate(score_names): is_active = tab_index == 0 html.append('' '{}'.format(tab_index, ' is-active' if is_active else '', self._FormatName(score_name))) html.append('
') html.append('
') html.append('
') # Tabs content. for tab_index, score_name in enumerate(score_names): html.append('
'.format( ' is-active' if is_active else '', tab_index)) html.append('
') html.append(self._BuildScoreTab(score_name, ('s{}'.format(tab_index),))) html.append('
') html.append('
') html.append('
') html.append('
') # Add snackbar for notifications. html.append( '
' '
' '' '
') return self._NEW_LINE.join(html) def _BuildScoreTab(self, score_name, anchor_data): """Builds the content of a tab.""" # Find unique values. scores = self._scores_data_frame[ self._scores_data_frame.eval_score_name == score_name] apm_configs = sorted(self._FindUniqueTuples(scores, ['apm_config'])) test_data_gen_configs = sorted(self._FindUniqueTuples( scores, ['test_data_gen', 'test_data_gen_params'])) html = [ '
', '
', '
', (''), ] # Header. html.append('') for test_data_gen_info in test_data_gen_configs: html.append(''.format( self._FormatName(test_data_gen_info[0]), test_data_gen_info[1])) html.append('') # Body. html.append('') for apm_config in apm_configs: html.append('') for test_data_gen_info in test_data_gen_configs: dialog_id = self._ScoreStatsInspectorDialogId( score_name, apm_config[0], test_data_gen_info[0], test_data_gen_info[1]) html.append( ''.format( dialog_id, self._BuildScoreTableCell( score_name, test_data_gen_info[0], test_data_gen_info[1], apm_config[0]))) html.append('') html.append('') html.append('
APM config / Test data generator{} {}
' + self._FormatName(apm_config[0]) + '{}
') html.append(self._BuildScoreStatsInspectorDialogs( score_name, apm_configs, test_data_gen_configs, anchor_data)) return self._NEW_LINE.join(html) def _BuildScoreTableCell(self, score_name, test_data_gen, test_data_gen_params, apm_config): """Builds the content of a table cell for a score table.""" scores = self._SliceDataForScoreTableCell( score_name, apm_config, test_data_gen, test_data_gen_params) stats = self._ComputeScoreStats(scores) html = [] items_id_prefix = ( score_name + test_data_gen + test_data_gen_params + apm_config) if stats['count'] == 1: # Show the only available score. item_id = hashlib.md5(items_id_prefix.encode('utf-8')).hexdigest() html.append('
{1:f}
'.format( item_id, scores['score'].mean())) html.append('
{}' '
'.format(item_id, 'single value')) else: # Show stats. for stat_name in ['min', 'max', 'mean', 'std dev']: item_id = hashlib.md5( (items_id_prefix + stat_name).encode('utf-8')).hexdigest() html.append('
{1:f}
'.format( item_id, stats[stat_name])) html.append('
{}' '
'.format(item_id, stat_name)) return self._NEW_LINE.join(html) def _BuildScoreStatsInspectorDialogs( self, score_name, apm_configs, test_data_gen_configs, anchor_data): """Builds a set of score stats inspector dialogs.""" html = [] for apm_config in apm_configs: for test_data_gen_info in test_data_gen_configs: dialog_id = self._ScoreStatsInspectorDialogId( score_name, apm_config[0], test_data_gen_info[0], test_data_gen_info[1]) html.append(''.format(dialog_id)) # Content. html.append('
') html.append('
APM config preset: {}
' 'Test data generator: {} ({})
'.format( self._FormatName(apm_config[0]), self._FormatName(test_data_gen_info[0]), test_data_gen_info[1])) html.append(self._BuildScoreStatsInspectorDialog( score_name, apm_config[0], test_data_gen_info[0], test_data_gen_info[1], anchor_data + (dialog_id,))) html.append('
') # Actions. html.append('
') html.append('') html.append('
') html.append('
') return self._NEW_LINE.join(html) def _BuildScoreStatsInspectorDialog( self, score_name, apm_config, test_data_gen, test_data_gen_params, anchor_data): """Builds one score stats inspector dialog.""" scores = self._SliceDataForScoreTableCell( score_name, apm_config, test_data_gen, test_data_gen_params) capture_render_pairs = sorted(self._FindUniqueTuples( scores, ['capture', 'render'])) echo_simulators = sorted(self._FindUniqueTuples(scores, ['echo_simulator'])) html = [''] # Header. html.append('') for echo_simulator in echo_simulators: html.append('') html.append('') # Body. html.append('') for row, (capture, render) in enumerate(capture_render_pairs): html.append(''.format( capture, render)) for col, echo_simulator in enumerate(echo_simulators): score_tuple = self._SliceDataForScoreStatsTableCell( scores, capture, render, echo_simulator[0]) cell_class = 'r{}c{}'.format(row, col) html.append(''.format( cell_class, self._BuildScoreStatsInspectorTableCell( score_tuple, anchor_data + (cell_class,)))) html.append('') html.append('') html.append('
Capture-Render / Echo simulator' + self._FormatName(echo_simulator[0]) +'
{}
{}
{}
') # Placeholder for the audio inspector. html.append('
') return self._NEW_LINE.join(html) def _BuildScoreStatsInspectorTableCell(self, score_tuple, anchor_data): """Builds the content of a cell of a score stats inspector.""" anchor = '&'.join(anchor_data) html = [('
{}
' '').format(score_tuple.score, anchor)] # Add all the available file paths as hidden data. for field_name in score_tuple.keys(): if field_name.endswith('_filepath'): html.append(''.format( field_name, score_tuple[field_name])) return self._NEW_LINE.join(html) def _SliceDataForScoreTableCell( self, score_name, apm_config, test_data_gen, test_data_gen_params): """Slices |self._scores_data_frame| to extract the data for a tab.""" masks = [] masks.append(self._scores_data_frame.eval_score_name == score_name) masks.append(self._scores_data_frame.apm_config == apm_config) masks.append(self._scores_data_frame.test_data_gen == test_data_gen) masks.append( self._scores_data_frame.test_data_gen_params == test_data_gen_params) mask = functools.reduce((lambda i1, i2: i1 & i2), masks) del masks return self._scores_data_frame[mask] @classmethod def _SliceDataForScoreStatsTableCell( cls, scores, capture, render, echo_simulator): """Slices |scores| to extract the data for a tab.""" masks = [] masks.append(scores.capture == capture) masks.append(scores.render == render) masks.append(scores.echo_simulator == echo_simulator) mask = functools.reduce((lambda i1, i2: i1 & i2), masks) del masks sliced_data = scores[mask] assert len(sliced_data) == 1, 'single score is expected' return sliced_data.iloc[0] @classmethod def _FindUniqueTuples(cls, data_frame, fields): """Slices |data_frame| to a list of fields and finds unique tuples.""" return data_frame[fields].drop_duplicates().values.tolist() @classmethod def _ComputeScoreStats(cls, data_frame): """Computes score stats.""" scores = data_frame['score'] return { 'count': scores.count(), 'min': scores.min(), 'max': scores.max(), 'mean': scores.mean(), 'std dev': scores.std(), } @classmethod def _ScoreStatsInspectorDialogId(cls, score_name, apm_config, test_data_gen, test_data_gen_params): """Assigns a unique name to a dialog.""" return 'score-stats-dialog-' + hashlib.md5( 'score-stats-inspector-{}-{}-{}-{}'.format( score_name, apm_config, test_data_gen, test_data_gen_params).encode('utf-8')).hexdigest() @classmethod def _Save(cls, output_filepath, html): """Writes the HTML file. Args: output_filepath: output file path. html: string with the HTML content. """ with open(output_filepath, 'w') as f: f.write(html) @classmethod def _FormatName(cls, name): """Formats a name. Args: name: a string. Returns: A copy of name in which underscores and dashes are replaced with a space. """ return re.sub(r'[_\-]', ' ', name)