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