xref: /aosp_15_r20/external/pigweed/pw_console/py/text_formatting_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 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"""Tests for pw_console.text_formatting"""
15
16import unittest
17from parameterized import parameterized  # type: ignore
18
19from prompt_toolkit.formatted_text import ANSI
20
21from pw_console.text_formatting import (
22    get_line_height,
23    insert_linebreaks,
24    split_lines,
25)
26
27
28class TestTextFormatting(unittest.TestCase):
29    """Tests for manipulating prompt_toolkit formatted text tuples."""
30
31    maxDiff = None
32
33    @parameterized.expand(
34        [
35            (
36                'with short prefix height 2',
37                len('LINE that should be wrapped'),  # text_width
38                len('|                |'),  # screen_width
39                len('--->'),  # prefix_width
40                ('LINE that should b\n' '--->e wrapped     \n').count(
41                    '\n'
42                ),  # expected_height
43                len('_____'),  # expected_trailing_characters
44            ),
45            (
46                'with short prefix height 3',
47                len('LINE that should be wrapped three times.'),  # text_width
48                len('|                |'),  # screen_width
49                len('--->'),  # prefix_width
50                (
51                    'LINE that should b\n'
52                    '--->e wrapped thre\n'
53                    '--->e times.      \n'
54                ).count(
55                    '\n'
56                ),  # expected_height
57                len('______'),  # expected_trailing_characters
58            ),
59            (
60                'with short prefix height 4',
61                len('LINE that should be wrapped even more times, say four.'),
62                len('|                |'),  # screen_width
63                len('--->'),  # prefix_width
64                (
65                    'LINE that should b\n'
66                    '--->e wrapped even\n'
67                    '---> more times, s\n'
68                    '--->ay four.      \n'
69                ).count(
70                    '\n'
71                ),  # expected_height
72                len('______'),  # expected_trailing_characters
73            ),
74            (
75                'no wrapping needed',
76                len('LINE wrapped'),  # text_width
77                len('|                |'),  # screen_width
78                len('--->'),  # prefix_width
79                ('LINE wrapped      \n').count('\n'),  # expected_height
80                len('______'),  # expected_trailing_characters
81            ),
82            (
83                'prefix is > screen width',
84                len('LINE that should be wrapped'),  # text_width
85                len('|                |'),  # screen_width
86                len('------------------>'),  # prefix_width
87                ('LINE that should b\n' 'e wrapped         \n').count(
88                    '\n'
89                ),  # expected_height
90                len('_________'),  # expected_trailing_characters
91            ),
92            (
93                'prefix is == screen width',
94                len('LINE that should be wrapped'),  # text_width
95                len('|                |'),  # screen_width
96                len('----------------->'),  # prefix_width
97                ('LINE that should b\n' 'e wrapped         \n').count(
98                    '\n'
99                ),  # expected_height
100                len('_________'),  # expected_trailing_characters
101            ),
102        ]
103    )
104    def test_get_line_height(
105        self,
106        _name,
107        text_width,
108        screen_width,
109        prefix_width,
110        expected_height,
111        expected_trailing_characters,
112    ) -> None:
113        """Test line height calculations."""
114        height, remaining_width = get_line_height(
115            text_width, screen_width, prefix_width
116        )
117        self.assertEqual(height, expected_height)
118        self.assertEqual(remaining_width, expected_trailing_characters)
119
120    # pylint: disable=line-too-long
121    @parameterized.expand(
122        [
123            (
124                'One line with ANSI escapes and no included breaks',
125                12,  # screen_width
126                False,  # truncate_long_lines
127                'Lorem ipsum \x1b[34m\x1b[1mdolor sit amet\x1b[0m, consectetur adipiscing elit.',  # message
128                ANSI(
129                    # Line 1
130                    'Lorem ipsum \n'
131                    # Line 2
132                    '\x1b[34m\x1b[1m'  # zero width
133                    'dolor sit am\n'
134                    # Line 3
135                    'et'
136                    '\x1b[0m'  # zero width
137                    ', consecte\n'
138                    # Line 4
139                    'tur adipisci\n'
140                    # Line 5
141                    'ng elit.\n'
142                ).__pt_formatted_text__(),
143                5,  # expected_height
144            ),
145            (
146                'One line with ANSI escapes and included breaks',
147                12,  # screen_width
148                False,  # truncate_long_lines
149                'Lorem\n ipsum \x1b[34m\x1b[1mdolor sit amet\x1b[0m, consectetur adipiscing elit.',  # message
150                ANSI(
151                    # Line 1
152                    'Lorem\n'
153                    # Line 2
154                    ' ipsum \x1b[34m\x1b[1mdolor\n'
155                    # Line 3
156                    ' sit amet\x1b[0m, c\n'
157                    # Line 4
158                    'onsectetur a\n'
159                    # Line 5
160                    'dipiscing el\n'
161                    # Line 6
162                    'it.\n'
163                ).__pt_formatted_text__(),
164                6,  # expected_height
165            ),
166            (
167                'One line with ANSI escapes and included breaks; truncate lines enabled',
168                12,  # screen_width
169                True,  # truncate_long_lines
170                'Lorem\n ipsum dolor sit amet, consectetur adipiscing \nelit.\n',  # message
171                ANSI(
172                    # Line 1
173                    'Lorem\n'
174                    # Line 2
175                    ' ipsum dolor\n'
176                    # Line 3
177                    'elit.\n'
178                ).__pt_formatted_text__(),
179                3,  # expected_height
180            ),
181            (
182                'wrapping enabled with a line break just after screen_width',
183                10,  # screen_width
184                False,  # truncate_long_lines
185                '01234567890\nTest Log\n',  # message
186                ANSI('0123456789\n' '0\n' 'Test Log\n').__pt_formatted_text__(),
187                3,  # expected_height
188            ),
189            (
190                'log message with a line break at screen_width',
191                10,  # screen_width
192                True,  # truncate_long_lines
193                '0123456789\nTest Log\n',  # message
194                ANSI('0123456789\n' 'Test Log\n').__pt_formatted_text__(),
195                2,  # expected_height
196            ),
197        ]
198    )
199    # pylint: enable=line-too-long
200    def test_insert_linebreaks(
201        self,
202        _name,
203        screen_width,
204        truncate_long_lines,
205        raw_text,
206        expected_fragments,
207        expected_height,
208    ) -> None:
209        """Test inserting linebreaks to wrap lines."""
210
211        formatted_text = ANSI(raw_text).__pt_formatted_text__()
212
213        fragments, line_height = insert_linebreaks(
214            formatted_text,
215            max_line_width=screen_width,
216            truncate_long_lines=truncate_long_lines,
217        )
218
219        self.assertEqual(fragments, expected_fragments)
220        self.assertEqual(line_height, expected_height)
221
222    @parameterized.expand(
223        [
224            (
225                'flattened split',
226                ANSI(
227                    'Lorem\n' ' ipsum dolor\n' 'elit.\n'
228                ).__pt_formatted_text__(),
229                [
230                    ANSI('Lorem').__pt_formatted_text__(),
231                    ANSI(' ipsum dolor').__pt_formatted_text__(),
232                    ANSI('elit.').__pt_formatted_text__(),
233                ],  # expected_lines
234            ),
235            (
236                'split fragments from insert_linebreaks',
237                insert_linebreaks(
238                    ANSI(
239                        'Lorem\n ipsum dolor sit amet, consectetur adipiscing elit.'
240                    ).__pt_formatted_text__(),
241                    max_line_width=15,
242                    # [0] for the fragments, [1] is line_height
243                    truncate_long_lines=False,
244                )[0],
245                [
246                    ANSI('Lorem').__pt_formatted_text__(),
247                    ANSI(' ipsum dolor si').__pt_formatted_text__(),
248                    ANSI('t amet, consect').__pt_formatted_text__(),
249                    ANSI('etur adipiscing').__pt_formatted_text__(),
250                    ANSI(' elit.').__pt_formatted_text__(),
251                ],
252            ),
253            (
254                'empty lines',
255                # Each line should have at least one StyleAndTextTuple but without
256                # an ending line break.
257                [
258                    ('', '\n'),
259                    ('', '\n'),
260                ],
261                [
262                    [('', '')],
263                    [('', '')],
264                ],
265            ),
266        ]
267    )
268    def test_split_lines(
269        self,
270        _name,
271        input_fragments,
272        expected_lines,
273    ) -> None:
274        """Test splitting flattened StyleAndTextTuples into a list of lines."""
275
276        result_lines = split_lines(input_fragments)
277
278        self.assertEqual(result_lines, expected_lines)
279
280
281if __name__ == '__main__':
282    unittest.main()
283