summaryrefslogtreecommitdiffstats
path: root/contrib/python/wcwidth/py3/tests/test_text_sizing.py
blob: b5e18085e359f6c4436e607081c8bc824986e6df (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""Tests for Text Sizing Protocol (OSC 66) support."""

# 3rd party
import pytest

# local
from wcwidth import (TextSizing,
                     TextSizingParams,
                     clip,
                     width,
                     wcswidth,
                     iter_sequences,
                     strip_sequences)
from wcwidth.text_sizing import TEXT_FIELD_MAPPING
from wcwidth.escape_sequences import TEXT_SIZING_PATTERN

_W_HI = TEXT_FIELD_MAPPING['w'].high
_N_HI = TEXT_FIELD_MAPPING['n'].high
_D_HI = TEXT_FIELD_MAPPING['d'].high

CONTROL_CODES_PARAMS_CASES = [
    ('x=2', "", "Unknown text sizing field 'x' in "),
    ('s=3:x=3', "s=3", "Unknown text sizing field 'x' in "),
    ('s=2:x=3:w=9', f"s=2:w={_W_HI}", "Unknown text sizing field 'x' in "),
    ('xyz=2', "", "Unknown text sizing field 'xyz' in "),
    ('xxx', "", "Expected '=' in text sizing parameter"),
    ('s=xxx', "", "Illegal text sizing value 'xxx' in "),
    ('s=-99', "", "Out of bounds text sizing value '-99' in "),
    ('s=99', f"s={_W_HI}", "Out of bounds text sizing value '99' in "),
    ('w=-1', "", "Out of bounds text sizing value '-1' in "),
    ('w=8', f"w={_W_HI}", "Out of bounds text sizing value '8' in "),
    ('n=20', f"n={_N_HI}", "Out of bounds text sizing value '20' in "),
    ('d=99', f"d={_D_HI}", "Out of bounds text sizing value '99' in "),
    ('v=5', "v=2", "Out of bounds text sizing value '5' in "),
    ('h=3', "h=2", "Out of bounds text sizing value '3' in "),
]


@pytest.mark.parametrize('given_params,expected_remainder,expected_exc,', CONTROL_CODES_PARAMS_CASES)
def test_text_sizing_params_control_codes(given_params, expected_remainder, expected_exc):
    """Verify control_codes='strict' and 'parse' behavior in TextSizingParams.from_params()."""
    # assert control_codes='strict' raises expected exception,
    with pytest.raises(ValueError) as exc_info:
        TextSizingParams.from_params(given_params, control_codes='strict')
    assert exc_info.value.args[0].startswith(expected_exc)

    # when 'parse' (default), any illegal argument or value is filtered, excluded, or clipped
    params = TextSizingParams.from_params(given_params)
    assert params.make_sequence() == expected_remainder


@pytest.mark.parametrize('given_params,expected_remainder,expected_exc,', CONTROL_CODES_PARAMS_CASES)
def test_text_sizing_width_control_codes(given_params, expected_remainder, expected_exc):
    """Verify control_codes='strict' with invalid OSC 66 sequences in wciwdth.width()."""
    seq1 = '\x1b]66;' + given_params + ';ABC' + '\x07'
    seq2 = '\x1b]66;' + given_params + ';ABC' + '\x1b\\'
    for seq in (seq1, seq2):
        with pytest.raises(ValueError) as exc_info:
            width(seq, control_codes='strict')
        assert exc_info.value.args[0].startswith(expected_exc)


@pytest.mark.parametrize('params,expected_repr', [
    (TextSizingParams(), 'TextSizingParams()'),
    (TextSizingParams(scale=2, width=1), 'TextSizingParams(scale=2, width=1)'),
    (TextSizingParams(scale=2, width=3, numerator=1, denominator=2,
                      vertical_align=1, horizontal_align=2),
     'TextSizingParams(scale=2, width=3, numerator=1, denominator=2, '
     'vertical_align=1, horizontal_align=2)'),
])
def test_text_sizing_params_repr(params, expected_repr):
    """Verify TextSizingParams.__repr__ output."""
    assert repr(params) == expected_repr


@pytest.mark.parametrize('params,text,expected_width', [
    # cases of static width=N values,
    (TextSizingParams(scale=2, width=1), 'climclam', 2),
    (TextSizingParams(scale=2, width=3), 'anything', 6),
    (TextSizingParams(scale=1, width=5), '', 5),
    (TextSizingParams(scale=3, width=1), 'x', 3),
    # and automatic width (width=0) values,
    (TextSizingParams(), '', 0),
    (TextSizingParams(), 'AB', 2),
    (TextSizingParams(), '中', 2),
    (TextSizingParams(scale=2), 'AB', 4),
    (TextSizingParams(scale=2), '中', 4),
    (TextSizingParams(scale=3), '', 0),
    (TextSizingParams(scale=7, width=7, numerator=15, denominator=15,
                      vertical_align=2, horizontal_align=2), 'x!yzzy', 49),
])
def test_text_sizing_width(params, text, expected_width):
    """Verify width using with both kinds of terminator."""
    # verify internal TextSizing.display_width() result,
    assert TextSizing(params, text, terminator='\x07').display_width() == expected_width
    assert TextSizing(params, text, terminator='\x1b\\').display_width() == expected_width
    seq1 = TextSizing(params, text, terminator='\x07').make_sequence()
    seq2 = TextSizing(params, text, terminator='\x1b\\').make_sequence()

    # verify round-trip
    ts_match1, ts_match2 = TEXT_SIZING_PATTERN.match(seq1), TEXT_SIZING_PATTERN.match(seq2)
    assert ts_match1 and ts_match2
    assert TextSizing.from_match(ts_match1) == TextSizing(params, text, terminator='\x07')
    assert TextSizing.from_match(ts_match2) == TextSizing(params, text, terminator='\x1b\\')

    # and external width(),
    assert width(seq1) == expected_width
    assert width(seq2) == expected_width

    # verify 'strict' does not raise ValueError
    width(seq1, control_codes='strict')
    width(seq2, control_codes='strict')

    # and verify 'ignore' measures only inner_text (does not parse scale or width)
    assert width(seq1, control_codes='ignore') == wcswidth(text)
    assert width(seq2, control_codes='ignore') == wcswidth(text)


@pytest.mark.parametrize('given_sequence,expected_text,expected_params,expected_width', [
    ('\x1b]66;s=2:w=2;AB\x07', 'AB', 's=2:w=2', 4),
    ('\x1b]66;s=2:w=2;\u4e2d\x07', '\u4e2d', 's=2:w=2', 4),
    ('\x1b]66;s=3:w=1;x\x07', 'x', 's=3:w=1', 3),
    ('\x1b]66;w=5;hello\x07', 'hello', 'w=5', 5),
    ('\x1b]66;s=2:w=3;anything\x07', 'anything', 's=2:w=3', 6),
    ('\x1b]66;w=3;x\x07', 'x', 'w=3', 3),
    ('\x1b]66;s=1;AB\x07', 'AB', '', 2),
    ('\x1b]66;s=2;AB\x07', 'AB', 's=2', 4),
    ('\x1b]66;s=2;中\x07', '中', 's=2', 4),
    ('\x1b]66;s=2;\x07', '', 's=2', 0),
    ('\x1b]66;s=1:w=1;\x07', '', 'w=1', 1),
    ('\x1b]66;w=2;A\x07', 'A', 'w=2', 2),
    ('\x1b]66;s=2:w=3;text\x1b\\', 'text', 's=2:w=3', 6),
])
def test_text_sizing_sequence(given_sequence, expected_text, expected_params, expected_width):
    """Verify parsing and measured width of raw OSC 66 sequence."""
    ts_match = TEXT_SIZING_PATTERN.match(given_sequence)
    assert ts_match is not None
    text_size = TextSizing.from_match(ts_match)
    assert text_size.params.make_sequence() == expected_params
    assert text_size.text == expected_text
    assert width(given_sequence, control_codes='parse') == expected_width
    assert width(given_sequence, control_codes='strict') == expected_width
    assert width(given_sequence, control_codes='ignore') == wcswidth(expected_text)


@pytest.mark.parametrize('text,expected', [
    ('\x1b]66;s=2:w=3:n=1:d=2:v=1:h=2;x!yzzy\x1b\\', 6),
    ('\x1b]66;s=2:w=3;anything\x07', 6),
    ('\x1b]66;w=3;x\x07', 3),
    ('\x1b]66;s=1:w=0;AB\x07', 2),
    ('\x1b]66;s=2:w=0;AB\x07', 4),
    ('\x1b]66;s=2:w=0;\u4e2d\x07', 4),  # '中'
    ('\x1b]66;s=1:w=0;\x07', 0),
    ('abc\x1b]66;w=3;x\x07def', 9),
    ('\x1b]66;w=2;A\x07\x1b]66;w=3;B\x07', 5),
    ('\x1b]66;s=2:w=3;text\x1b\\', 6),
    ('\x1b[31m\x1b]66;w=2;AB\x07\x1b[0m', 2),
])
def test_strings_with_text_sizing(text, expected):
    """Verify measured width strings containing OSC66."""
    assert width(text) == expected
    assert width(text, control_codes='strict') == expected


@pytest.mark.parametrize('text,expected', [
    ('\x1b]66;s=2;hello\x07', 'hello'),
    ('\x1b]66;s=2;hello\x1b\\', 'hello'),
    ('\x1b]66;;text\x07', 'text'),
    ('\x1b]66;s=3:w=2;\x07', ''),
    ('abc\x1b]66;w=2;XY\x07def', 'abcXYdef'),
    ('\x1b[31m\x1b]66;s=2;red\x07\x1b[0m', 'red'),
    ('\x1b]66;w=1;A\x07\x1b]66;w=1;B\x07', 'AB'),
])
def test_strip_strings_with_text_sizing(text, expected):
    assert strip_sequences(text) == expected


@pytest.mark.parametrize('text,expected_segs', [
    ('abc\x1b]66;s=2;hello\x07def', [('abc', False), ('\x1b]66;s=2;hello\x07', True), ('def', False)]),
    ('abc\x1b]66;s=2;n=1,d=2,w=3;hello\x1b\\def', [('abc', False), ('\x1b]66;s=2;n=1,d=2,w=3;hello\x1b\\', True), ('def', False)]),
])
def test_iter_sequences_text_sizing(text, expected_segs):
    assert list(iter_sequences(text)) == expected_segs


@pytest.mark.parametrize('text,start,end,expected', [
    ('\x1b]66;w=3;ABC\x07', 0, 3, '\x1b]66;w=3;ABC\x07'),
    ('\x1b]66;w=3;ABC\x07', 0, 2, '\x1b]66;w=2;AB\x07'),
    ('\x1b]66;w=3;ABC\x07', 1, 3, '\x1b]66;w=2;BC\x07'),
    ('ab\x1b]66;w=2;XY\x07cd', 0, 6, 'ab\x1b]66;w=2;XY\x07cd'),
    ('ab\x1b]66;w=2;XY\x07cd', 0, 3, 'ab\x1b]66;w=1;X\x07'),
    ('ab\x1b]66;w=2;XY\x07cd', 3, 6, '\x1b]66;w=1;Y\x07cd'),
    ('ab\x1b]66;w=2;XY\x07cd', 4, 6, 'cd'),
])
def test_clip_text_sizing_basic(text, start, end, expected):
    """Test basic support of clip() with text sizing sequence."""
    assert repr(clip(text, start, end)) == repr(expected)


@pytest.mark.parametrize('text,start,end,expected', [
    ('\x1b]66;s=2;ABC\x07', 0, 0, ''),
    ('\x1b]66;s=2;ABC\x07', 6, 6, ''),
    ('\x1b]66;s=2;ABC\x07', 0, 2, '\x1b]66;s=2;A\x07'),
    ('\x1b]66;s=2;ABC\x07', 0, 4, '\x1b]66;s=2;AB\x07'),
    ('\x1b]66;s=2;ABC\x07', 0, 6, '\x1b]66;s=2;ABC\x07'),
    ('\x1b]66;s=2;ABC\x07', 2, 6, '\x1b]66;s=2;BC\x07'),
    ('\x1b]66;s=2;ABC\x07', 4, 6, '\x1b]66;s=2;C\x07'),
])
def test_clip_text_sizing_scaled(text, start, end, expected):
    """Test support of clip() with scale=N arguments."""
    assert repr(clip(text, start, end)) == repr(expected)


@pytest.mark.parametrize('text,start,end,expected', [
    #  a   b   c
    # === === ===
    # 012 345 678
    # .
    # ..
    # *a*
    # *a* .
    # ... *b*
    # ... *b* .
    # ... *b* ..
    # ... *b* *c*
    ('\x1b]66;s=3;ABC\x07', 0, 0, ''),
    ('\x1b]66;s=3;ABC\x07', 0, 1, '.'),
    ('\x1b]66;s=3;ABC\x07', 0, 2, '..'),
    ('\x1b]66;s=3;ABC\x07', 0, 3, '\x1b]66;s=3;A\x07'),
    ('\x1b]66;s=3;ABC\x07', 0, 4, '\x1b]66;s=3;A\x07.'),
    ('\x1b]66;s=3;ABC\x07', 0, 5, '\x1b]66;s=3;A\x07..'),
    ('\x1b]66;s=3;ABC\x07', 0, 6, '\x1b]66;s=3;AB\x07'),
    ('\x1b]66;s=3;ABC\x07', 0, 7, '\x1b]66;s=3;AB\x07.'),
    ('\x1b]66;s=3;ABC\x07', 0, 8, '\x1b]66;s=3;AB\x07..'),
    ('\x1b]66;s=3;ABC\x07', 0, 9, '\x1b]66;s=3;ABC\x07'),
    ('\x1b]66;s=3;ABC\x07', 0, 10, '\x1b]66;s=3;ABC\x07'),
    #  a   b
    # === === ===
    # 012 345 678
    #  .             1, 2
    #  ..            1, 3
    #  .. .          1, 4
    #  .. ..         1, 5
    #  .. *b*        1, 6
    #  .. *b* .      1, 7
    #  .. *b* ..     1, 8
    #  .. *b* *c*    1, 9
    ('\x1b]66;s=3;ABC\x07', 1, 1, ''),
    ('\x1b]66;s=3;ABC\x07', 1, 2, '.'),
    ('\x1b]66;s=3;ABC\x07', 1, 3, '..'),
    ('\x1b]66;s=3;ABC\x07', 1, 4, '...'),
    ('\x1b]66;s=3;ABC\x07', 1, 5, '....'),
    ('\x1b]66;s=3;ABC\x07', 1, 6, '..\x1b]66;s=3;B\x07'),
    ('\x1b]66;s=3;ABC\x07', 1, 7, '..\x1b]66;s=3;B\x07.'),
    ('\x1b]66;s=3;ABC\x07', 1, 8, '..\x1b]66;s=3;B\x07..'),
    ('\x1b]66;s=3;ABC\x07', 1, 9, '..\x1b]66;s=3;BC\x07'),
    ('\x1b]66;s=3;ABC\x07', 1, 10, '..\x1b]66;s=3;BC\x07'),
    # two-thirds of string 'A' and half of string 'B' is fillchar
    # ('\x1b]66;s=3;ABC\x07', 2, 4, '..'),
    # half of string 'A' and all of string 'B'
    #  a   b
    # === === ===
    # 012 345 678
    #   .            2, 3
    #   . .          2, 4
    #   . ..         2, 5
    #   . *b*        2, 6
    #   . *b* .      2, 7
    #   . *b* ..     2, 8
    #   . *b* *c*    2, 9
    ('\x1b]66;s=3;ABC\x07', 2, 2, ''),
    ('\x1b]66;s=3;ABC\x07', 2, 3, '.'),
    ('\x1b]66;s=3;ABC\x07', 2, 4, '..'),
    ('\x1b]66;s=3;ABC\x07', 2, 5, '...'),
    ('\x1b]66;s=3;ABC\x07', 2, 6, '.\x1b]66;s=3;B\x07'),
    ('\x1b]66;s=3;ABC\x07', 2, 7, '.\x1b]66;s=3;B\x07.'),
    ('\x1b]66;s=3;ABC\x07', 2, 8, '.\x1b]66;s=3;B\x07..'),
    ('\x1b]66;s=3;ABC\x07', 2, 9, '.\x1b]66;s=3;BC\x07'),
    ('\x1b]66;s=3;ABC\x07', 2, 10, '.\x1b]66;s=3;BC\x07'),
    # and now 3:10, should be easy ...
    ('\x1b]66;s=3;ABC\x07', 3, 3, ''),
    ('\x1b]66;s=3;ABC\x07', 3, 4, '.'),
    ('\x1b]66;s=3;ABC\x07', 3, 5, '..'),
    ('\x1b]66;s=3;ABC\x07', 3, 6, '\x1b]66;s=3;B\x07'),
    ('\x1b]66;s=3;ABC\x07', 3, 7, '\x1b]66;s=3;B\x07.'),
    ('\x1b]66;s=3;ABC\x07', 3, 8, '\x1b]66;s=3;B\x07..'),
    ('\x1b]66;s=3;ABC\x07', 3, 9, '\x1b]66;s=3;BC\x07'),
    ('\x1b]66;s=3;ABC\x07', 3, 10, '\x1b]66;s=3;BC\x07'),
])
def test_clip_text_sizing_scaled_with_fillchar(text, start, end, expected):
    """Test support of clip() with scale=N and fillchar is needed to fill remainder."""
    assert repr(clip(text, start, end, fillchar='.')) == repr(expected)


def test_clip_simple_path_padding():
    """Simple-path clip with w=N larger than text length exercises padding loop."""
    # w=4 but only 1 grapheme 'X' — 3 empty units are padded.
    # Clip window (0, 1) forces partial overlap, triggering
    # _text_sizing_clip_simple's padding branch.
    assert repr(clip('\x1b]66;w=4;X\x07', 0, 1)) == repr('\x1b]66;w=1;X\x07')


@pytest.mark.parametrize('text,start,end,expected', [
    # CR forces painter path; fully-visible text sizing sequence
    ('\r\x1b]66;w=2;XY\x07', 0, 3, '\x1b]66;w=2;XY\x07'),
    # CR painter path, text sizing partially clipped (first unit visible)
    ('\r\x1b]66;w=2;XY\x07', 0, 1, '\x1b]66;w=1;X\x07'),
    # BS forces painter path; text sizing fully visible
    ('ab\b\b\x1b]66;w=2;XY\x07', 0, 4, '\x1b]66;w=2;XY\x07'),
    # Painter path with partial text sizing overlap (exercises _text_sizing_clip_painter)
    ('\ra\x1b]66;s=2;BC\x07', 0, 3, 'a\x1b]66;s=2;B\x07'),
    # Painter path: text sizing scaled partial overlap with fillchar
    ('\r\x1b]66;s=3;ABC\x07', 1, 6, '  \x1b]66;s=3;B\x07'),
    # CSI movement + text sizing fully visible
    ('ab\x1b[2D\x1b]66;w=2;XY\x07', 0, 4, '\x1b]66;w=2;XY\x07'),
    # Painter path: text sizing entirely outside clip window (before start)
    ('\r\x1b]66;w=2;XY\x07', 2, 4, ''),
    # CR + text sizing with auto-width (w=0), partial overlap
    ('\ra\x1b]66;s=2;BC\x07', 0, 5, 'a\x1b]66;s=2;BC\x07'),
    # Painter path: padding when w=N has more units than graphemes
    ('\r\x1b]66;w=3;A\x07', 0, 2, '\x1b]66;w=2;A\x07'),
    # Painter path: text sizing with unit entirely before clip window (skip path)
    ('\r\x1b]66;s=2;ABCD\x07', 4, 8, '\x1b]66;s=2;CD\x07'),
])
def test_clip_text_sizing_painter(text, start, end, expected):
    """Test clip() with text sizing sequences in the cursor-movement (painter) path."""
    assert repr(clip(text, start, end)) == repr(expected)