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