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