blob: d203717aa96cc005bacd8d941b18c2f670fd7143 [file] [log] [blame]
# 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,
Union)
from pw_bloat.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')