2019-11-14 21:49:52 +00:00
|
|
|
# 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
|
2019-11-15 00:36:15 +00:00
|
|
|
from typing import Callable, Collection, Dict, List, Optional, Tuple, Type
|
2019-12-10 18:19:27 +00:00
|
|
|
from typing import Union
|
2019-11-14 21:49:52 +00:00
|
|
|
|
|
|
|
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."""
|
2019-12-16 18:39:49 +00:00
|
|
|
|
2019-11-14 21:49:52 +00:00
|
|
|
@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 = '═'
|
|
|
|
|
|
|
|
|
2019-11-15 00:36:15 +00:00
|
|
|
def identity(val: str) -> str:
|
|
|
|
"""Returns a string unmodified."""
|
|
|
|
return val
|
|
|
|
|
|
|
|
|
2019-11-14 21:49:52 +00:00
|
|
|
class TableOutput(Output):
|
|
|
|
"""Tabular output."""
|
|
|
|
|
|
|
|
LABEL_COLUMN = 'Label'
|
|
|
|
|
2019-12-04 17:12:15 +00:00
|
|
|
def __init__(
|
2019-12-16 18:39:49 +00:00
|
|
|
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'):
|
2019-11-14 21:49:52 +00:00
|
|
|
self._cs = charset
|
2019-11-15 00:36:15 +00:00
|
|
|
self._preprocess = preprocess
|
|
|
|
self._justify = justify
|
|
|
|
|
2019-11-14 21:49:52 +00:00
|
|
|
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):
|
2019-11-15 00:36:15 +00:00
|
|
|
val = self._preprocess(val)
|
2019-11-14 21:49:52 +00:00
|
|
|
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
|
|
|
|
|
2019-12-04 17:12:15 +00:00
|
|
|
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
|
2019-11-14 21:49:52 +00:00
|
|
|
|
|
|
|
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 ''
|
2019-11-15 00:36:15 +00:00
|
|
|
subrow.append(getattr(label, self._justify)(max_label, ' '))
|
2019-12-04 17:12:15 +00:00
|
|
|
subrow.extend([
|
|
|
|
getattr(self._preprocess(val),
|
|
|
|
self._justify)(column_widths[i], ' ')
|
|
|
|
for i, val in enumerate(segment)
|
|
|
|
])
|
2019-11-14 21:49:52 +00:00
|
|
|
subrows.append(self._table_row(subrow))
|
|
|
|
|
|
|
|
rows.append('\n'.join(subrows))
|
2019-12-04 17:12:15 +00:00
|
|
|
rows.append(separators['bot' if row == len(self._diffs) -
|
|
|
|
1 else 'mid'])
|
2019-11-14 21:49:52 +00:00
|
|
|
|
|
|
|
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] = ()):
|
2019-11-15 00:36:15 +00:00
|
|
|
# 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}'
|
|
|
|
|
2019-12-04 17:12:15 +00:00
|
|
|
super().__init__(None,
|
|
|
|
diffs,
|
|
|
|
AsciiCharset,
|
|
|
|
preprocess=add_rst_block,
|
|
|
|
justify='ljust')
|