1# Copyright 2019 The Pigweed Authors
2#
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
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
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
13# the License.
14"""The binary_diff module defines a class which stores size diff information."""
15
16import collections
17import csv
18
19from typing import List, Generator, Type
20
21DiffSegment = collections.namedtuple(
22    'DiffSegment', ['name', 'before', 'after', 'delta', 'capacity'])
23FormattedDiff = collections.namedtuple('FormattedDiff',
24                                       ['segment', 'before', 'delta', 'after'])
25
26
27def format_integer(num: int, force_sign: bool = False) -> str:
28    """Formats a integer with commas."""
29    prefix = '+' if force_sign and num > 0 else ''
30    return '{}{:,}'.format(prefix, num)
31
32
33def format_percent(num: float, force_sign: bool = False) -> str:
34    """Formats a decimal ratio as a percentage."""
35    prefix = '+' if force_sign and num > 0 else ''
36    return '{}{:,.1f}%'.format(prefix, num * 100)
37
38
39class BinaryDiff:
40    """A size diff between two binary files."""
41    def __init__(self, label: str):
42        self.label = label
43        self._segments: collections.OrderedDict = collections.OrderedDict()
44
45    def add_segment(self, segment: DiffSegment):
46        """Adds a segment to the diff."""
47        self._segments[segment.name] = segment
48
49    def formatted_segments(self) -> Generator[FormattedDiff, None, None]:
50        """Yields each of the segments in this diff with formatted data."""
51
52        if not self._segments:
53            yield FormattedDiff('(all)', '(same)', '0', '(same)')
54            return
55
56        has_diff_segment = False
57
58        for segment in self._segments.values():
59            if segment.delta == 0:
60                continue
61
62            has_diff_segment = True
63            yield FormattedDiff(
64                segment.name,
65                format_integer(segment.before),
66                format_integer(segment.delta, force_sign=True),
67                format_integer(segment.after),
68            )
69
70        if not has_diff_segment:
71            yield FormattedDiff('(all)', '(same)', '0', '(same)')
72
73    @classmethod
74    def from_csv(cls: Type['BinaryDiff'], label: str,
75                 raw_csv: List[str]) -> 'BinaryDiff':
76        """Parses a BinaryDiff from bloaty's CSV output."""
77
78        diff = cls(label)
79        reader = csv.reader(raw_csv)
80        for row in reader:
81            diff.add_segment(
82                DiffSegment(row[0], int(row[5]), int(row[7]), int(row[1]),
83                            int(row[3])))
84
85        return diff
86