1 # Copyright 2019 The Pigweed Authors
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 # use this file except in compliance with the License. You may obtain a copy of
7 # https://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations under
14 """Module containing different output formatters for the bloat script."""
18 from typing import Callable, Collection, Dict, List, Optional, Tuple, Type
19 from typing import Union
21 from binary_diff import BinaryDiff, FormattedDiff
24 class Output(abc.ABC):
25 """An Output produces a size report card in a specific format."""
28 diffs: Collection[BinaryDiff] = ()):
33 def diff(self) -> str:
34 """Creates a report card for a size diff between binaries and a base."""
37 def absolute(self) -> str:
38 """Creates a report card for the absolute size breakdown of binaries."""
41 class AsciiCharset(enum.Enum):
42 """Set of ASCII characters for drawing tables."""
57 class LineCharset(enum.Enum):
58 """Set of line-drawing characters for tables."""
73 def identity(val: str) -> str:
74 """Returns a string unmodified."""
78 class TableOutput(Output):
81 LABEL_COLUMN = 'Label'
86 diffs: Collection[BinaryDiff] = (),
87 charset: Union[Type[AsciiCharset],
88 Type[LineCharset]] = AsciiCharset,
89 preprocess: Callable[[str], str] = identity,
90 # TODO(frolv): Make this a Literal type.
91 justify: str = 'rjust'):
93 self._preprocess = preprocess
94 self._justify = justify
96 super().__init__(title, diffs)
98 def diff(self) -> str:
99 """Build a tabular diff output showing binary size deltas."""
101 # Calculate the width of each column in the table.
102 max_label = len(self.LABEL_COLUMN)
103 column_widths = [len(field) for field in FormattedDiff._fields]
105 for diff in self._diffs:
106 max_label = max(max_label, len(diff.label))
107 for segment in diff.formatted_segments():
108 for i, val in enumerate(segment):
109 val = self._preprocess(val)
110 column_widths[i] = max(column_widths[i], len(val))
112 separators = self._row_separators([max_label] + column_widths)
114 def title_pad(string: str) -> str:
115 padding = (len(separators['top']) - len(string)) // 2
116 return ' ' * padding + string
119 self._center_align(val.capitalize(), column_widths[i])
120 for i, val in enumerate(FormattedDiff._fields)
122 column_names = [self._center_align(self.LABEL_COLUMN, max_label)
127 if self._title is not None:
129 title_pad(self._title),
130 title_pad(self._cs.H.value * len(self._title)),
135 self._table_row(column_names),
139 for row, diff in enumerate(self._diffs):
140 subrows: List[str] = []
142 for segment in diff.formatted_segments():
143 subrow: List[str] = []
144 label = diff.label if not subrows else ''
145 subrow.append(getattr(label, self._justify)(max_label, ' '))
147 getattr(self._preprocess(val),
148 self._justify)(column_widths[i], ' ')
149 for i, val in enumerate(segment)
151 subrows.append(self._table_row(subrow))
153 rows.append('\n'.join(subrows))
154 rows.append(separators['bot' if row == len(self._diffs) -
157 return '\n'.join(rows)
159 def absolute(self) -> str:
162 def _row_separators(self, column_widths: List[int]) -> Dict[str, str]:
163 """Returns row separators for a table based on the character set."""
165 # Left, middle, and right characters for each of the separator rows.
166 top = (self._cs.TL.value, self._cs.TM.value, self._cs.TR.value)
167 mid = (self._cs.ML.value, self._cs.MM.value, self._cs.MR.value)
168 bot = (self._cs.BL.value, self._cs.BM.value, self._cs.BR.value)
170 def sep(chars: Tuple[str, str, str], heading: bool = False) -> str:
171 line = self._cs.HH.value if heading else self._cs.H.value
172 lines = [line * width for width in column_widths]
173 left = f'{chars[0]}{line}'
174 mid = f'{line}{chars[1]}{line}'.join(lines)
175 right = f'{line}{chars[2]}'
176 return f'{left}{mid}{right}'
180 'hdg': sep(mid, True),
185 def _table_row(self, vals: Collection[str]) -> str:
186 """Formats a row of the table with the selected character set."""
187 vert = self._cs.V.value
188 main = f' {vert} '.join(vals)
189 return f'{vert} {main} {vert}'
192 def _center_align(val: str, width: int) -> str:
193 """Left and right pads a value with spaces to center within a width."""
194 space = width - len(val)
195 padding = ' ' * (space // 2)
196 extra = ' ' if space % 2 == 1 else ''
197 return f'{extra}{padding}{val}{padding}'
200 class RstOutput(TableOutput):
201 """Tabular output in ASCII format, which is also valid RST."""
202 def __init__(self, diffs: Collection[BinaryDiff] = ()):
203 # Use RST line blocks within table cells to force each value to appear
204 # on a new line in the HTML output.
205 def add_rst_block(val: str) -> str:
208 super().__init__(None,
211 preprocess=add_rst_block,