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