| # Copyright 2019 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Module containing different output formatters for the bloat script.""" |
| |
| import abc |
| import enum |
| from typing import Callable, Collection, Dict, List, Optional, Tuple, Type |
| from typing import Union |
| |
| from binary_diff import BinaryDiff, FormattedDiff |
| |
| |
| class Output(abc.ABC): |
| """An Output produces a size report card in a specific format.""" |
| def __init__(self, |
| title: Optional[str], |
| diffs: Collection[BinaryDiff] = ()): |
| self._title = title |
| self._diffs = diffs |
| |
| @abc.abstractmethod |
| def diff(self) -> str: |
| """Creates a report card for a size diff between binaries and a base.""" |
| |
| @abc.abstractmethod |
| def absolute(self) -> str: |
| """Creates a report card for the absolute size breakdown of binaries.""" |
| |
| |
| class AsciiCharset(enum.Enum): |
| """Set of ASCII characters for drawing tables.""" |
| TL = '+' |
| TM = '+' |
| TR = '+' |
| ML = '+' |
| MM = '+' |
| MR = '+' |
| BL = '+' |
| BM = '+' |
| BR = '+' |
| V = '|' |
| H = '-' |
| HH = '=' |
| |
| |
| class LineCharset(enum.Enum): |
| """Set of line-drawing characters for tables.""" |
| TL = '┌' |
| TM = '┬' |
| TR = '┐' |
| ML = '├' |
| MM = '┼' |
| MR = '┤' |
| BL = '└' |
| BM = '┴' |
| BR = '┘' |
| V = '│' |
| H = '─' |
| HH = '═' |
| |
| |
| def identity(val: str) -> str: |
| """Returns a string unmodified.""" |
| return val |
| |
| |
| class TableOutput(Output): |
| """Tabular output.""" |
| |
| LABEL_COLUMN = 'Label' |
| |
| def __init__( |
| self, |
| title: Optional[str], |
| diffs: Collection[BinaryDiff] = (), |
| charset: Union[Type[AsciiCharset], Type[LineCharset]] = AsciiCharset, |
| preprocess: Callable[[str], str] = identity, |
| # TODO(frolv): Make this a Literal type. |
| justify: str = 'rjust'): |
| self._cs = charset |
| self._preprocess = preprocess |
| self._justify = justify |
| |
| super().__init__(title, diffs) |
| |
| def diff(self) -> str: |
| """Build a tabular diff output showing binary size deltas.""" |
| |
| # Calculate the width of each column in the table. |
| max_label = len(self.LABEL_COLUMN) |
| column_widths = [len(field) for field in FormattedDiff._fields] |
| |
| for diff in self._diffs: |
| max_label = max(max_label, len(diff.label)) |
| for segment in diff.formatted_segments(): |
| for i, val in enumerate(segment): |
| val = self._preprocess(val) |
| column_widths[i] = max(column_widths[i], len(val)) |
| |
| separators = self._row_separators([max_label] + column_widths) |
| |
| def title_pad(string: str) -> str: |
| padding = (len(separators['top']) - len(string)) // 2 |
| return ' ' * padding + string |
| |
| titles = [ |
| self._center_align(val.capitalize(), column_widths[i]) |
| for i, val in enumerate(FormattedDiff._fields) |
| ] |
| column_names = [self._center_align(self.LABEL_COLUMN, max_label) |
| ] + titles |
| |
| rows: List[str] = [] |
| |
| if self._title is not None: |
| rows.extend([ |
| title_pad(self._title), |
| title_pad(self._cs.H.value * len(self._title)), |
| ]) |
| |
| rows.extend([ |
| separators['top'], |
| self._table_row(column_names), |
| separators['hdg'], |
| ]) |
| |
| for row, diff in enumerate(self._diffs): |
| subrows: List[str] = [] |
| |
| for segment in diff.formatted_segments(): |
| subrow: List[str] = [] |
| label = diff.label if not subrows else '' |
| subrow.append(getattr(label, self._justify)(max_label, ' ')) |
| subrow.extend([ |
| getattr(self._preprocess(val), |
| self._justify)(column_widths[i], ' ') |
| for i, val in enumerate(segment) |
| ]) |
| subrows.append(self._table_row(subrow)) |
| |
| rows.append('\n'.join(subrows)) |
| rows.append(separators['bot' if row == len(self._diffs) - |
| 1 else 'mid']) |
| |
| return '\n'.join(rows) |
| |
| def absolute(self) -> str: |
| return '' |
| |
| def _row_separators(self, column_widths: List[int]) -> Dict[str, str]: |
| """Returns row separators for a table based on the character set.""" |
| |
| # Left, middle, and right characters for each of the separator rows. |
| top = (self._cs.TL.value, self._cs.TM.value, self._cs.TR.value) |
| mid = (self._cs.ML.value, self._cs.MM.value, self._cs.MR.value) |
| bot = (self._cs.BL.value, self._cs.BM.value, self._cs.BR.value) |
| |
| def sep(chars: Tuple[str, str, str], heading: bool = False) -> str: |
| line = self._cs.HH.value if heading else self._cs.H.value |
| lines = [line * width for width in column_widths] |
| left = f'{chars[0]}{line}' |
| mid = f'{line}{chars[1]}{line}'.join(lines) |
| right = f'{line}{chars[2]}' |
| return f'{left}{mid}{right}' |
| |
| return { |
| 'top': sep(top), |
| 'hdg': sep(mid, True), |
| 'mid': sep(mid), |
| 'bot': sep(bot), |
| } |
| |
| def _table_row(self, vals: Collection[str]) -> str: |
| """Formats a row of the table with the selected character set.""" |
| vert = self._cs.V.value |
| main = f' {vert} '.join(vals) |
| return f'{vert} {main} {vert}' |
| |
| @staticmethod |
| def _center_align(val: str, width: int) -> str: |
| """Left and right pads a value with spaces to center within a width.""" |
| space = width - len(val) |
| padding = ' ' * (space // 2) |
| extra = ' ' if space % 2 == 1 else '' |
| return f'{extra}{padding}{val}{padding}' |
| |
| |
| class RstOutput(TableOutput): |
| """Tabular output in ASCII format, which is also valid RST.""" |
| def __init__(self, diffs: Collection[BinaryDiff] = ()): |
| # Use RST line blocks within table cells to force each value to appear |
| # on a new line in the HTML output. |
| def add_rst_block(val: str) -> str: |
| return f'| {val}' |
| |
| super().__init__(None, |
| diffs, |
| AsciiCharset, |
| preprocess=add_rst_block, |
| justify='ljust') |