diff options
| author | robot-piglet <[email protected]> | 2026-02-11 20:50:19 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-02-11 21:10:56 +0300 |
| commit | c377d73a156c964ac8c78b11a1e75fa667d26da5 (patch) | |
| tree | d61be80413be895e7a4e2d2057004982b2f7b0c7 /contrib/python/wcwidth/py3/tests | |
| parent | 09047cebd54187751addb4fa71a9d3a9fa0f19b1 (diff) | |
Intermediate changes
commit_hash:260a33021d6afd633fd9e204567374069239f62a
Diffstat (limited to 'contrib/python/wcwidth/py3/tests')
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_benchmarks.py | 59 | ||||
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_clip.py | 45 | ||||
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_core.py | 14 | ||||
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_emojis.py | 25 | ||||
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_sgr_state.py | 238 | ||||
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_textwrap.py | 138 | ||||
| -rw-r--r-- | contrib/python/wcwidth/py3/tests/test_ucslevel.py | 207 |
7 files changed, 508 insertions, 218 deletions
diff --git a/contrib/python/wcwidth/py3/tests/test_benchmarks.py b/contrib/python/wcwidth/py3/tests/test_benchmarks.py index 6e0ffadcc45..9506d9887c2 100644 --- a/contrib/python/wcwidth/py3/tests/test_benchmarks.py +++ b/contrib/python/wcwidth/py3/tests/test_benchmarks.py @@ -198,6 +198,41 @@ def test_wrap_with_ansi(benchmark): benchmark(wcwidth.wrap, text, 20) +def test_wrap_with_ansi_no_propagate(benchmark): + """Benchmark wrap() with ANSI but SGR propagation disabled.""" + text = '\x1b[31mThe quick brown fox\x1b[0m jumps over the lazy dog' + benchmark(wcwidth.wrap, text, 20, propagate_sgr=False) + + +def test_wrap_complex_sgr(benchmark): + """Benchmark wrap() with complex SGR (256-color, multiple attributes).""" + text = '\x1b[1;3;38;5;208mBold italic orange text that wraps\x1b[0m' + benchmark(wcwidth.wrap, text, 10) + + +def test_wrap_hyperlink_no_id(benchmark): + """Benchmark wrap() with OSC 8 hyperlinks without id (requires id generation).""" + link = '\x1b]8;;https://example.com/path\x1b\\click here for details\x1b]8;;\x1b\\' + text = f'See {link} and also {link} for more. Read {link} now. ' * 10 + benchmark(wcwidth.wrap, text, 40) + + +def test_wrap_hyperlink_with_id(benchmark): + """Benchmark wrap() with OSC 8 hyperlinks with existing ids.""" + link1 = '\x1b]8;id=a;https://example.com\x1b\\click here for details\x1b]8;;\x1b\\' + link2 = '\x1b]8;id=b;https://other.org\x1b\\visit this page now\x1b]8;;\x1b\\' + text = f'See {link1} and also {link2} for more. Read {link1} now. ' * 10 + benchmark(wcwidth.wrap, text, 40) + + +def test_wrap_hyperlink_mixed(benchmark): + """Benchmark wrap() with mixed OSC 8 hyperlinks (with and without ids).""" + no_id = '\x1b]8;;https://example.com\x1b\\link without id here\x1b]8;;\x1b\\' + with_id = '\x1b]8;id=x;https://other.org\x1b\\link with id here\x1b]8;;\x1b\\' + text = f'First {no_id} then {with_id} and {no_id} again {with_id} end. ' * 10 + benchmark(wcwidth.wrap, text, 40) + + def test_clip_ascii(benchmark): """Benchmark clip() with ASCII string.""" benchmark(wcwidth.clip, 'hello world', 0, 5) @@ -214,6 +249,30 @@ def test_clip_with_ansi(benchmark): benchmark(wcwidth.clip, text, 0, 3) +def test_clip_with_ansi_no_propagate(benchmark): + """Benchmark clip() with ANSI but SGR propagation disabled.""" + text = '\x1b[31m中文字\x1b[0m' + benchmark(wcwidth.clip, text, 0, 3, propagate_sgr=False) + + +def test_clip_complex_sgr(benchmark): + """Benchmark clip() with complex SGR clipping from middle.""" + text = '\x1b[1;38;5;208mHello world text\x1b[0m' + benchmark(wcwidth.clip, text, 6, 11) + + +def test_propagate_sgr_multiline(benchmark): + """Benchmark propagate_sgr() with multiple lines.""" + lines = ['\x1b[1;31mline one', 'line two', 'line three\x1b[0m'] + benchmark(wcwidth.propagate_sgr, lines) + + +def test_propagate_sgr_no_sequences(benchmark): + """Benchmark propagate_sgr() fast path (no sequences).""" + lines = ['line one', 'line two', 'line three'] + benchmark(wcwidth.propagate_sgr, lines) + + def test_strip_sequences_simple(benchmark): """Benchmark strip_sequences() with simple ANSI codes.""" text = '\x1b[31mred\x1b[0m' diff --git a/contrib/python/wcwidth/py3/tests/test_clip.py b/contrib/python/wcwidth/py3/tests/test_clip.py index 8a98c14c6f2..995d383a8ac 100644 --- a/contrib/python/wcwidth/py3/tests/test_clip.py +++ b/contrib/python/wcwidth/py3/tests/test_clip.py @@ -111,15 +111,24 @@ def test_clip_sequences_before_start(): def test_clip_sequences_after_end(): - assert clip('hello\x1b[31m world\x1b[0m', 0, 5) == 'hello\x1b[31m\x1b[0m' + # With propagate_sgr=True (default), no style active at start, so no prefix + assert clip('hello\x1b[31m world\x1b[0m', 0, 5) == 'hello' + # With propagate_sgr=False, all sequences preserved + assert clip('hello\x1b[31m world\x1b[0m', 0, 5, propagate_sgr=False) == 'hello\x1b[31m\x1b[0m' def test_clip_sequences_multiple(): - assert clip('\x1b[1m\x1b[31mbold red\x1b[0m', 0, 4) == '\x1b[1m\x1b[31mbold\x1b[0m' + # With propagate_sgr=True (default), sequences collapsed to minimal + assert clip('\x1b[1m\x1b[31mbold red\x1b[0m', 0, 4) == '\x1b[1;31mbold\x1b[0m' + # With propagate_sgr=False, all sequences preserved separately + assert clip('\x1b[1m\x1b[31mbold red\x1b[0m', 0, 4, propagate_sgr=False) == '\x1b[1m\x1b[31mbold\x1b[0m' def test_clip_sequences_only(): - assert clip('\x1b[31m\x1b[0m', 0, 10) == '\x1b[31m\x1b[0m' + # With propagate_sgr=True (default), no visible text means empty result + assert clip('\x1b[31m\x1b[0m', 0, 10) == '' + # With propagate_sgr=False, sequences preserved + assert clip('\x1b[31m\x1b[0m', 0, 10, propagate_sgr=False) == '\x1b[31m\x1b[0m' def test_clip_sequences_osc_hyperlink(): @@ -131,6 +140,10 @@ def test_clip_sequences_cjk_with_sequences(): assert clip('\x1b[31m中文\x1b[0m', 0, 3) == '\x1b[31m中 \x1b[0m' +def test_clip_sequences_partial_wide_at_start(): + assert clip('\x1b[31m中文\x1b[0m', 1, 4) == '\x1b[31m 文\x1b[0m' + + def test_clip_sequences_between_chars(): assert clip('a\x1b[31mb\x1b[0mc', 1, 2) == '\x1b[31mb\x1b[0m' @@ -167,6 +180,27 @@ def test_clip_combining_multiple(): assert clip('e\u0301\u0327', 0, 1) == 'e\u0301\u0327' +def test_clip_zero_width_position_bounds(): + # Standalone combining mark before visible region should NOT be included + assert clip('\u0301hello', 1, 4) == 'ell' + # Standalone combining mark after visible region should NOT be included + assert clip('hello\u0301', 0, 3) == 'hel' + # Combining mark within visible region should be included (attached to base) + assert clip('he\u0301llo', 0, 4) == 'he\u0301ll' + + +def test_clip_prepend_grapheme(): + # PREPEND characters (Arabic Number Sign) cluster with following char, width 2 + # Full cluster fits + assert clip('\u0600abc', 0, 2) == '\u0600a' + # Cluster split at start boundary - replaced with fillchar + assert clip('\u0600abc', 0, 1) == ' ' + # Cluster split at end boundary - partial overlap gets fillchar + assert clip('\u0600abc', 1, 3) == ' b' + # Clipping after the prepend cluster + assert clip('\u0600abc', 2, 4) == 'bc' + + def test_clip_ambiguous_width_1(): assert clip('\u00b1test', 0, 3, ambiguous_width=1) == '\u00b1te' @@ -221,3 +255,8 @@ CLIP_CURSOR_SEQUENCE_CASES = [ @pytest.mark.parametrize('text,start,end,expected', CLIP_CURSOR_SEQUENCE_CASES) def test_clip_cursor_sequences_zero_width(text, start, end, expected): assert clip(text, start, end) == expected + + +def test_clip_tab_first_visible_with_sgr(): + """Tab as first visible character with SGR propagation.""" + assert clip('\x1b[31m\tb', 0, 4, tabsize=8) == '\x1b[31m \x1b[0m' diff --git a/contrib/python/wcwidth/py3/tests/test_core.py b/contrib/python/wcwidth/py3/tests/test_core.py index b8ae3611430..fef89f53ce5 100644 --- a/contrib/python/wcwidth/py3/tests/test_core.py +++ b/contrib/python/wcwidth/py3/tests/test_core.py @@ -379,20 +379,6 @@ def test_kannada_script_2(): assert length_phrase == expect_length_phrase -def test_zero_wide_conflict(): - # Test characters considered both "wide" and "zero" width - # - (0x03000, 0x0303e,), # Ideographic Space ..Ideographic Variation In - # + (0x03000, 0x03029,), # Ideographic Space ..Hangzhou Numeral Nine - assert wcwidth.wcwidth(chr(0x03029), unicode_version='4.1.0') == 2 - assert wcwidth.wcwidth(chr(0x0302a), unicode_version='4.1.0') == 0 - - # - (0x03099, 0x030ff,), # Combining Katakana-hirag..Katakana Digraph Koto - # + (0x0309b, 0x030ff,), # Katakana-hiragana Voiced..Katakana Digraph Koto - assert wcwidth.wcwidth(chr(0x03099), unicode_version='4.1.0') == 0 - assert wcwidth.wcwidth(chr(0x0309a), unicode_version='4.1.0') == 0 - assert wcwidth.wcwidth(chr(0x0309b), unicode_version='4.1.0') == 2 - - def test_soft_hyphen(): # Test SOFT HYPHEN, category 'Cf' usually are zero-width, but most # implementations agree to draw it was '1' cell, visually diff --git a/contrib/python/wcwidth/py3/tests/test_emojis.py b/contrib/python/wcwidth/py3/tests/test_emojis.py index 615d6d2f6b5..3f85877dec2 100644 --- a/contrib/python/wcwidth/py3/tests/test_emojis.py +++ b/contrib/python/wcwidth/py3/tests/test_emojis.py @@ -188,8 +188,8 @@ def test_recommended_variation_16_sequences(benchmark): assert len(sequences) >= 742 -def test_unicode_9_vs16(): - """Verify effect of VS-16 on unicode_version 9.0 and later.""" +def test_vs16_effect(): + """Verify effect of VS-16 (always active with latest Unicode version).""" phrase = ("\u2640" # FEMALE SIGN "\uFE0F") # VARIATION SELECTOR-16 @@ -197,25 +197,8 @@ def test_unicode_9_vs16(): expect_length_phrase = 2 # exercise, - length_each = tuple(wcwidth.wcwidth(w_char, unicode_version='9.0') for w_char in phrase) - length_phrase = wcwidth.wcswidth(phrase, unicode_version='9.0') - - # verify. - assert length_each == expect_length_each - assert length_phrase == expect_length_phrase - - -def test_unicode_8_vs16(): - """Verify that VS-16 has no effect on unicode_version 8.0 and earlier.""" - phrase = ("\u2640" # FEMALE SIGN - "\uFE0F") # VARIATION SELECTOR-16 - - expect_length_each = (1, 0) - expect_length_phrase = 1 - - # exercise, - length_each = tuple(wcwidth.wcwidth(w_char, unicode_version='8.0') for w_char in phrase) - length_phrase = wcwidth.wcswidth(phrase, unicode_version='8.0') + length_each = tuple(wcwidth.wcwidth(w_char) for w_char in phrase) + length_phrase = wcwidth.wcswidth(phrase) # verify. assert length_each == expect_length_each diff --git a/contrib/python/wcwidth/py3/tests/test_sgr_state.py b/contrib/python/wcwidth/py3/tests/test_sgr_state.py new file mode 100644 index 00000000000..acddfd8ff51 --- /dev/null +++ b/contrib/python/wcwidth/py3/tests/test_sgr_state.py @@ -0,0 +1,238 @@ +"""Tests for SGR state tracking and propagation.""" +from __future__ import annotations + +# std imports +import re + +# local +from wcwidth import clip, wrap +from wcwidth.sgr_state import (_SGR_STATE_DEFAULT, + _SGRState, + propagate_sgr, + _sgr_state_update, + _sgr_state_is_active, + _sgr_state_to_sequence) + + +def test_wrap_propagates_sgr(): + """Wrap() propagates SGR codes across lines by default.""" + assert wrap('\x1b[1;34mHello world\x1b[0m', width=6) == [ + '\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m'] + assert wrap('\x1b[1mHello world\x1b[0m', width=6) == [ + '\x1b[1mHello\x1b[0m', '\x1b[1mworld\x1b[0m'] + assert wrap('\x1b[31mred text here\x1b[0m', width=5) == [ + '\x1b[31mred\x1b[0m', '\x1b[31mtext\x1b[0m', '\x1b[31mhere\x1b[0m'] + assert wrap('\x1b[38;5;208morange text\x1b[0m', width=7) == [ + '\x1b[38;5;208morange\x1b[0m', '\x1b[38;5;208mtext\x1b[0m'] + assert wrap('\x1b[38;2;255;0;0mred text\x1b[0m', width=5) == [ + '\x1b[38;2;255;0;0mred\x1b[0m', '\x1b[38;2;255;0;0mtext\x1b[0m'] + # multiple attributes combined + result = wrap('\x1b[1;3;34mbold italic blue\x1b[0m', width=5) + assert result[0] == '\x1b[1;3;34mbold\x1b[0m' + assert result[1].startswith('\x1b[') and result[1].endswith('\x1b[0m') + + +def test_wrap_reset_and_no_sgr(): + """Wrap() handles reset and plain text.""" + assert wrap('\x1b[31mred\x1b[0m plain text', width=6) == ['\x1b[31mred\x1b[0m', 'plain', 'text'] + assert wrap('hello world', width=6) == ['hello', 'world'] + + +def test_wrap_propagate_sgr_disabled(): + """Wrap() with propagate_sgr=False returns old behavior.""" + assert wrap('\x1b[31mhello world\x1b[0m', width=6, propagate_sgr=False) == [ + '\x1b[31mhello', 'world\x1b[0m'] + + +def test_wrap_preserves_non_sgr_sequences(): + """Wrap() preserves non-SGR sequences (OSC hyperlinks).""" + result = wrap('\x1b]8;;url\x07long link text\x1b]8;;\x07', width=5) + # Hyperlinks get IDs added for identity preservation across lines + osc_pattern = re.compile(r'\x1b]8;[^;]*;url\x07') + assert all(osc_pattern.search(line) for line in result) + + +def test_clip_propagates_sgr(): + """Clip() restores SGR state at start and reset at end.""" + assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11) == '\x1b[1;34mworld\x1b[0m' + assert clip('\x1b[1mHello world\x1b[0m', 6, 11) == '\x1b[1mworld\x1b[0m' + assert clip('\x1b[31mHello world\x1b[0m', 6, 11) == '\x1b[31mworld\x1b[0m' + assert clip('\x1b[31mHello\x1b[0m', 0, 5) == '\x1b[31mHello\x1b[0m' + + +def test_clip_no_active_style_and_plain(): + """Clip() handles reset and plain text.""" + assert clip('\x1b[31mred\x1b[0m plain', 4, 9) == 'plain' + assert clip('Hello world', 6, 11) == 'world' + + +def test_clip_propagate_sgr_disabled(): + """Clip() with propagate_sgr=False returns old behavior.""" + assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11, propagate_sgr=False) == '\x1b[1;34mworld\x1b[0m' + + +def test_clip_preserves_non_sgr_sequences(): + """Clip() preserves non-SGR sequences (OSC hyperlinks).""" + result = clip('\x1b]8;;url\x07link\x1b]8;;\x07', 0, 4) + assert '\x1b]8;;url\x07' in result and '\x1b]8;;\x07' in result + + +def test_clip_sgr_only_no_visible_content(): + """Clip() returns empty when only SGR sequences exist.""" + assert clip('\x1b[31m\x1b[0m', 0, 10) == '' + + +def test_sgr_state_parse_boolean_attributes_on(): + """_sgr_state_update parses all boolean attribute on codes.""" + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1m').bold is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[2m').dim is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[3m').italic is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[4m').underline is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[5m').blink is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[6m').rapid_blink is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[7m').inverse is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[8m').hidden is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[9m').strikethrough is True + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[21m').double_underline is True + + +def test_sgr_state_parse_boolean_attributes_off(): + """_sgr_state_update parses all boolean attribute off codes.""" + assert _sgr_state_update(_SGRState(italic=True), '\x1b[23m').italic is False + assert _sgr_state_update(_SGRState(inverse=True), '\x1b[27m').inverse is False + assert _sgr_state_update(_SGRState(hidden=True), '\x1b[28m').hidden is False + assert _sgr_state_update(_SGRState(strikethrough=True), '\x1b[29m').strikethrough is False + # code 22 resets both bold and dim + state = _sgr_state_update(_SGRState(bold=True, dim=True), '\x1b[22m') + assert state.bold is False and state.dim is False + # code 24 resets both underline and double_underline + state = _sgr_state_update(_SGRState(underline=True, double_underline=True), '\x1b[24m') + assert state.underline is False and state.double_underline is False + # code 25 resets both blink and rapid_blink + state = _sgr_state_update(_SGRState(blink=True, rapid_blink=True), '\x1b[25m') + assert state.blink is False and state.rapid_blink is False + + +def test_sgr_state_parse_colors(): + """_sgr_state_update parses all color formats.""" + # basic foreground/background + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[31m').foreground == (31,) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[41m').background == (41,) + # bright foreground/background + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[91m').foreground == (91,) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[101m').background == (101,) + # 256-color (semicolon format) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m').foreground == (38, 5, 208) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;5;208m').background == (48, 5, 208) + # RGB (semicolon format) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255;128;0m').foreground == (38, 2, 255, 128, 0) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;2;255;128;0m').background == (48, 2, 255, 128, 0) + + +def test_sgr_state_parse_colors_colon_format(): + """_sgr_state_update parses ITU T.416 colon-separated color format.""" + # 256-color (colon format) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:5:208m').foreground == (38, 5, 208) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48:5:208m').background == (48, 5, 208) + # RGB with empty colorspace (colon format): 38:2::R:G:B + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:2::255:128:0m').foreground == (38, 2, 0, 255, 128, 0) + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48:2::255:128:0m').background == (48, 2, 0, 255, 128, 0) + # RGB with colorspace (colon format): 38:2:colorspace:R:G:B + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:2:1:255:128:0m').foreground == (38, 2, 1, 255, 128, 0) + # Mixed: colon color with semicolon attributes + state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1;38:2::255:0:0;4m') + assert state.bold is True + assert state.underline is True + assert state.foreground == (38, 2, 0, 255, 0, 0) + + +def test_sgr_state_color_override(): + """Newer color replaces older regardless of format.""" + state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m') + assert state.foreground == (38, 5, 208) + state = _sgr_state_update(state, '\x1b[31m') + assert state.foreground == (31,) + state = _sgr_state_update(state, '\x1b[38;2;0;255;0m') + assert state.foreground == (38, 2, 0, 255, 0) + state = _sgr_state_update(state, '\x1b[38;5;99m') + assert state.foreground == (38, 5, 99) + + +def test_sgr_state_parse_default_colors(): + """_sgr_state_update parses default color codes (39, 49).""" + assert _sgr_state_update(_SGRState(foreground=(31,)), '\x1b[39m').foreground is None + assert _sgr_state_update(_SGRState(background=(41,)), '\x1b[49m').background is None + + +def test_sgr_state_parse_compound_and_reset(): + """_sgr_state_update handles compound sequences and reset.""" + state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1;34;3m') + assert state.bold is True and state.italic is True and state.foreground == (34,) + # reset with 0 + state = _sgr_state_update(_SGRState(bold=True, foreground=(31,)), '\x1b[0m') + assert state == _SGR_STATE_DEFAULT + # empty is reset + state = _sgr_state_update(_SGRState(bold=True), '\x1b[m') + assert state == _SGR_STATE_DEFAULT + # empty param in compound treated as 0 + state = _sgr_state_update(_SGRState(bold=True), '\x1b[;1m') + assert state.bold is True + + +def test_sgr_state_to_sequence(): + """_sgr_state_to_sequence generates correct sequences.""" + assert _sgr_state_to_sequence(_SGR_STATE_DEFAULT) == '' + assert _sgr_state_to_sequence(_SGRState(bold=True)) == '\x1b[1m' + assert _sgr_state_to_sequence(_SGRState(rapid_blink=True)) == '\x1b[6m' + assert _sgr_state_to_sequence(_SGRState(double_underline=True)) == '\x1b[21m' + assert _sgr_state_to_sequence(_SGRState(foreground=(31,))) == '\x1b[31m' + assert _sgr_state_to_sequence(_SGRState(foreground=(38, 5, 208))) == '\x1b[38;5;208m' + assert _sgr_state_to_sequence(_SGRState(foreground=(38, 2, 255, 128, 0))) == '\x1b[38;2;255;128;0m' + assert _sgr_state_to_sequence(_SGRState(bold=True, italic=True, foreground=(34,))) == '\x1b[1;3;34m' + + +def test_sgr_state_to_sequence_all_attributes(): + """_sgr_state_to_sequence handles all attributes.""" + state = _SGRState(bold=True, italic=True, underline=True, inverse=True, + foreground=(31,), background=(44,)) + seq = _sgr_state_to_sequence(state) + assert '\x1b[' in seq and 'm' in seq + assert all(code in seq for code in ('1', '3', '4', '7', '31', '44')) + + +def test_sgr_state_is_active(): + """_sgr_state_is_active detects active state.""" + assert _sgr_state_is_active(_SGR_STATE_DEFAULT) is False + assert _sgr_state_is_active(_SGRState(bold=True)) is True + assert _sgr_state_is_active(_SGRState(foreground=(31,))) is True + + +def test_propagate_sgr(): + """propagate_sgr handles various input cases.""" + assert len(propagate_sgr([])) == 0 + assert propagate_sgr(['hello', 'world']) == ['hello', 'world'] + assert propagate_sgr(['\x1b[31mhello\x1b[0m']) == ['\x1b[31mhello\x1b[0m'] + assert propagate_sgr(['\x1b[31mhello', 'world\x1b[0m']) == [ + '\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m'] + assert propagate_sgr(['\x1b[31mred\x1b[0m', 'plain']) == ['\x1b[31mred\x1b[0m', 'plain'] + + +def test_propagate_sgr_empty_lines(): + """propagate_sgr handles empty lines.""" + result = propagate_sgr(['\x1b[31mred', '', 'text\x1b[0m']) + assert result[0] == '\x1b[31mred\x1b[0m' + assert result[1] == '\x1b[31m\x1b[0m' + assert result[2] == '\x1b[31mtext\x1b[0m' + + +def test_sgr_malformed_sequences(): + """Malformed SGR sequences are ignored.""" + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5m').foreground is None + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255m').foreground is None + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[999m') == _SGR_STATE_DEFAULT + # malformed background extended colors + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;5m').background is None + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;2;255m').background is None + # invalid mode (not 2 or 5) for extended color + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;3;128m').foreground is None + assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;3;128m').background is None diff --git a/contrib/python/wcwidth/py3/tests/test_textwrap.py b/contrib/python/wcwidth/py3/tests/test_textwrap.py index fc15f1917f9..eb12621ba3f 100644 --- a/contrib/python/wcwidth/py3/tests/test_textwrap.py +++ b/contrib/python/wcwidth/py3/tests/test_textwrap.py @@ -11,6 +11,20 @@ import pytest from wcwidth import iter_sequences from wcwidth.textwrap import SequenceTextWrapper, wrap + [email protected](autouse=True) +def mock_hyperlink_ids(monkeypatch): + """Mock secrets.token_hex to return predictable IDs for testing.""" + counter = 0 + + def fake_token_hex(n): + nonlocal counter + counter += 1 + return f'{counter:0{n * 2}x}' + + monkeypatch.setattr('secrets.token_hex', fake_token_hex) + + SGR_RED = '\x1b[31m' SGR_BLUE = '\x1b[34m' SGR_BOLD = '\x1b[1m' @@ -103,6 +117,12 @@ HYPHEN_LONG_WORD_CASES = [ ('a-b-c-d', 3, False, ['a-b', '-c-', 'd']), ('---', 2, True, ['--', '-']), ('a---b', 2, True, ['a-', '--', 'b']), + # With propagate_sgr=True, SGR continues to next line + ('a-\x1b[31mb', 2, True, ['a-\x1b[31m\x1b[0m', '\x1b[31mb\x1b[0m']), +] + +HYPHEN_LONG_WORD_CASES_NO_PROPAGATE = [ + # With propagate_sgr=False, SGR stays where it is ('a-\x1b[31mb', 2, True, ['a-\x1b[31m', 'b']), ] @@ -112,6 +132,11 @@ def test_wrap_hyphen_long_words(text, w, break_hyphens, expected): assert wrap(text, w, break_on_hyphens=break_hyphens) == expected [email protected]('text,w,break_hyphens,expected', HYPHEN_LONG_WORD_CASES_NO_PROPAGATE) +def test_wrap_hyphen_long_words_no_propagate(text, w, break_hyphens, expected): + assert wrap(text, w, break_on_hyphens=break_hyphens, propagate_sgr=False) == expected + + # Comprehensive stdlib compatibility TEXTWRAP_KWARGS = [ {'break_long_words': False, 'drop_whitespace': False}, @@ -196,24 +221,37 @@ def test_wrap_unicode(benchmark, text, w, expected): assert result == expected -# Escape sequence preservation +# Escape sequence preservation (with propagate_sgr=True default) SEQUENCE_CASES = [ - # SGR sequences preserved at word boundaries + # SGR sequences propagated across lines (f'{SGR_RED}red{SGR_RESET} blue', 4, [f'{SGR_RED}red{SGR_RESET}', 'blue']), - (f'hello{SGR_RED} world', 6, [f'hello{SGR_RED}', 'world']), - # Empty/adjacent sequences + # SGR at end of line propagates to next line + (f'hello{SGR_RED} world', 6, [f'hello{SGR_RED}{SGR_RESET}', f'{SGR_RED}world{SGR_RESET}']), + # Empty/adjacent sequences - sequences kept (no visible content) (f'{SGR_RED}{SGR_RESET}', 10, [f'{SGR_RED}{SGR_RESET}']), + # Reset clears style, sequences kept with second line (f'hello {SGR_RED}{SGR_RESET}world', 6, ['hello', f'{SGR_RED}{SGR_RESET}world']), - # OSC hyperlinks (with space separator) + # OSC hyperlinks (with space separator) - not SGR, so preserved as-is (f'{OSC_HYPERLINK} text', 5, [OSC_HYPERLINK, 'text']), - # CSI cursor sequences + # CSI cursor sequences - not SGR, so preserved as-is (f'{CSI_CURSOR}text here', 10, [f'{CSI_CURSOR}text', 'here']), # Control characters (f'{CTRL_BEL}alert text', 6, [f'{CTRL_BEL}alert', 'text']), - # Sequences in long word breaking - ('x\x1b[31mabcdefghij\x1b[0m', 3, ['xab', 'cde', 'fgh', 'ij']), - # Lone ESC - ('abc\x1bdefghij', 3, ['abc', 'def', 'ghi', 'j']), + # Sequences in long word breaking - red starts after 'x', continues across lines + ('x\x1b[31mabcdefghij\x1b[0m', 3, + ['x\x1b[31mab\x1b[0m', '\x1b[31mcde\x1b[0m', '\x1b[31mfgh\x1b[0m', '\x1b[31mij\x1b[0m']), + # Lone ESC - not a valid SGR sequence, stays with preceding text + ('abc\x1bdefghij', 3, ['abc\x1b', 'def', 'ghi', 'j']), +] + +# Old behavior tests (propagate_sgr=False) +SEQUENCE_CASES_NO_PROPAGATE = [ + (f'{SGR_RED}red{SGR_RESET} blue', 4, [f'{SGR_RED}red{SGR_RESET}', 'blue']), + (f'hello{SGR_RED} world', 6, [f'hello{SGR_RED}', 'world']), + (f'{SGR_RED}{SGR_RESET}', 10, [f'{SGR_RED}{SGR_RESET}']), + (f'hello {SGR_RED}{SGR_RESET}world', 6, ['hello', f'{SGR_RED}{SGR_RESET}world']), + # Sequences preserved where they are, not propagated + ('x\x1b[31mabcdefghij\x1b[0m', 3, ['x\x1b[31mab', 'cde', 'fgh', 'ij\x1b[0m']), ] @@ -223,7 +261,13 @@ def test_wrap_sequences(benchmark, text, w, expected): if any('\x1b' in e or '\x00' <= e[0] < '\x20' for e in expected if e): assert result == expected else: - assert [_strip(line) for line in result] == expected + assert result == expected + + [email protected]('text,w,expected', SEQUENCE_CASES_NO_PROPAGATE) +def test_wrap_sequences_no_propagate(text, w, expected): + result = wrap(text, w, propagate_sgr=False) + assert result == expected # Mixed: sequences + unicode @@ -321,6 +365,78 @@ HYPERLINK_WORD_BOUNDARY_CASES = [ 6, ['foo', f'{OSC_START_BEL}{SGR_RED}link{SGR_RESET}{OSC_END_BEL}', 'bar'], ), + ( # hyperlink with internal space - breaks with id continuation (ST) + f'Go {OSC_START_ST}Click here{OSC_END_ST} now', + 5, + [ + 'Go', + '\x1b]8;id=00000001;http://example.com\x1b\\Click\x1b]8;;\x1b\\', + '\x1b]8;id=00000001;http://example.com\x1b\\here\x1b]8;;\x1b\\', + 'now', + ], + ), + ( # hyperlink with internal space - breaks with id continuation (BEL) + f'Go {OSC_START_BEL}Click here{OSC_END_BEL} now', + 5, + [ + 'Go', + '\x1b]8;id=00000001;http://example.com\x07Click\x1b]8;;\x07', + '\x1b]8;id=00000001;http://example.com\x07here\x1b]8;;\x07', + 'now', + ], + ), + ( # hyperlink with existing id= parameter is preserved + '\x1b]8;id=my-link;http://example.com\x1b\\Click here\x1b]8;;\x1b\\', + 6, + [ + '\x1b]8;id=my-link;http://example.com\x1b\\Click\x1b]8;;\x1b\\', + '\x1b]8;id=my-link;http://example.com\x1b\\here\x1b]8;;\x1b\\', + ], + ), + ( # hyperlink spanning 3+ lines + f'{OSC_START_ST}one two three{OSC_END_ST}', + 5, + [ + '\x1b]8;id=00000001;http://example.com\x1b\\one\x1b]8;;\x1b\\', + '\x1b]8;id=00000001;http://example.com\x1b\\two\x1b]8;;\x1b\\', + '\x1b]8;id=00000001;http://example.com\x1b\\three\x1b]8;;\x1b\\', + ], + ), + ( # multiple hyperlinks in same text + f'{OSC_START_ST}ab cd{OSC_END_ST} {OSC_START_BEL}ef gh{OSC_END_BEL}', + 4, + [ + '\x1b]8;id=00000001;http://example.com\x1b\\ab\x1b]8;;\x1b\\', + '\x1b]8;id=00000001;http://example.com\x1b\\cd\x1b]8;;\x1b\\', + '\x1b]8;id=00000002;http://example.com\x07ef\x1b]8;;\x07', + '\x1b]8;id=00000002;http://example.com\x07gh\x1b]8;;\x07', + ], + ), + ( # long word inside hyperlink forces character-level breaking + f'{OSC_START_ST}abcdefgh{OSC_END_ST}', + 3, + [ + '\x1b]8;id=00000001;http://example.com\x1b\\abc\x1b]8;;\x1b\\', + '\x1b]8;id=00000001;http://example.com\x1b\\def\x1b]8;;\x1b\\', + '\x1b]8;id=00000001;http://example.com\x1b\\gh\x1b]8;;\x1b\\', + ], + ), + ( # params with other keys but no id - id is prepended, other params preserved + '\x1b]8;foo=bar;http://example.com\x1b\\Click here\x1b]8;;\x1b\\', + 6, + [ + '\x1b]8;id=00000001:foo=bar;http://example.com\x1b\\Click\x1b]8;;\x1b\\', + '\x1b]8;id=00000001:foo=bar;http://example.com\x1b\\here\x1b]8;;\x1b\\', + ], + ), + ( # id not at start of params (junk:id=given) - full params preserved + '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\Click here\x1b]8;;\x1b\\', + 6, + [ + '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\Click\x1b]8;;\x1b\\', + '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\here\x1b]8;;\x1b\\', + ], + ), ] diff --git a/contrib/python/wcwidth/py3/tests/test_ucslevel.py b/contrib/python/wcwidth/py3/tests/test_ucslevel.py index a907db2c44a..979cfe0fe8d 100644 --- a/contrib/python/wcwidth/py3/tests/test_ucslevel.py +++ b/contrib/python/wcwidth/py3/tests/test_ucslevel.py @@ -1,189 +1,58 @@ """Unicode version level tests for wcwidth.""" -# std imports -import warnings - -# 3rd party -import pytest - # local import wcwidth +def test_list_versions_single(): + """list_versions returns only the latest version.""" + versions = wcwidth.list_versions() + assert len(versions) == 1 + assert versions[0] == "17.0.0" + + def test_latest(): - """wcwidth._wcmatch_version('latest') returns tail item.""" - # given, + """wcwidth._wcmatch_version('latest') returns the latest version.""" expected = wcwidth.list_versions()[-1] - - # exercise, result = wcwidth._wcmatch_version('latest') - - # verify. - assert result == expected - - -def test_exact_410_str(): - """wcwidth._wcmatch_version('4.1.0') returns equal value (str).""" - # given, - given = expected = '4.1.0' - - # exercise, - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_exact_410_unicode(): - """wcwidth._wcmatch_version(u'4.1.0') returns equal value (unicode).""" - # given, - given = expected = '4.1.0' - - # exercise, - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_nearest_505_str(): - """ - wcwidth._wcmatch_version('5.0.5') returns nearest '5.0.0'. - - (str) - """ - # given - given, expected = '5.0.5', '5.0.0' - - # exercise - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_nearest_505_unicode(): - """ - wcwidth._wcmatch_version(u'5.0.5') returns nearest u'5.0.0'. - - (unicode) - """ - # given - given, expected = '5.0.5', '5.0.0' - - # exercise - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_nearest_lowint40_str(): - """wcwidth._wcmatch_version('4.0') returns nearest '4.1.0'.""" - # given - given, expected = '4.0', '4.1.0' - warnings.resetwarnings() - wcwidth._wcmatch_version.cache_clear() - - # exercise - with pytest.warns(UserWarning): - # warns that given version is lower than any available - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_nearest_lowint40_unicode(): - """wcwidth._wcmatch_version(u'4.0') returns nearest u'4.1.0'.""" - # given - given, expected = '4.0', '4.1.0' - warnings.resetwarnings() - wcwidth._wcmatch_version.cache_clear() - - # exercise - with pytest.warns(UserWarning): - # warns that given version is lower than any available - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_nearest_800_str(): - """wcwidth._wcmatch_version('8') returns nearest '8.0.0'.""" - # given - given, expected = '8', '8.0.0' - - # exercise - result = wcwidth._wcmatch_version(given) - - # verify. assert result == expected -def test_nearest_800_unicode(): - """wcwidth._wcmatch_version(u'8') returns nearest u'8.0.0'.""" - # given - given, expected = '8', '8.0.0' - - # exercise - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - - -def test_nearest_999_str(): - """wcwidth._wcmatch_version('999.0') returns nearest (latest).""" - # given - given, expected = '999.0', wcwidth.list_versions()[-1] - - # exercise - result = wcwidth._wcmatch_version(given) - - # verify. +def test_auto(): + """wcwidth._wcmatch_version('auto') returns the latest version.""" + expected = wcwidth.list_versions()[-1] + result = wcwidth._wcmatch_version('auto') assert result == expected -def test_nearest_999_unicode(): - """wcwidth._wcmatch_version(u'999.0') returns nearest (latest).""" - # given - given, expected = '999.0', wcwidth.list_versions()[-1] +def test_wcmatch_version_always_latest(): + """_wcmatch_version always returns latest regardless of input.""" + latest = wcwidth.list_versions()[-1] + assert wcwidth._wcmatch_version('4.1.0') == latest + assert wcwidth._wcmatch_version('9.0.0') == latest + assert wcwidth._wcmatch_version('auto') == latest + assert wcwidth._wcmatch_version('latest') == latest + assert wcwidth._wcmatch_version('invalid') == latest + assert wcwidth._wcmatch_version('999.0.0') == latest + assert wcwidth._wcmatch_version('x.y.z') == latest + assert wcwidth._wcmatch_version('8') == latest + assert wcwidth._wcmatch_version('5.0.5') == latest - # exercise - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected - -def test_nonint_unicode(): - """wcwidth._wcmatch_version(u'x.y.z') returns latest (unicode).""" - # given - given, expected = 'x.y.z', wcwidth.list_versions()[-1] - warnings.resetwarnings() +def test_env_var_ignored(monkeypatch): + """UNICODE_VERSION environment variable is ignored.""" + monkeypatch.setenv('UNICODE_VERSION', '4.1.0') wcwidth._wcmatch_version.cache_clear() + assert wcwidth._wcmatch_version('auto') == wcwidth.list_versions()[-1] - # exercise - with pytest.warns(UserWarning): - # warns that given version is not valid - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected +def test_unicode_version_param_ignored(): + """unicode_version parameter is ignored in wcwidth/wcswidth.""" + # wcwidth should work the same regardless of unicode_version value + assert wcwidth.wcwidth('A', unicode_version='4.1.0') == 1 + assert wcwidth.wcwidth('A', unicode_version='latest') == 1 + assert wcwidth.wcwidth('A', unicode_version='invalid') == 1 -def test_nonint_str(): - """wcwidth._wcmatch_version(u'x.y.z') returns latest (str).""" - # given - given, expected = 'x.y.z', wcwidth.list_versions()[-1] - warnings.resetwarnings() - wcwidth._wcmatch_version.cache_clear() - - # exercise - with pytest.warns(UserWarning): - # warns that given version is not valid - result = wcwidth._wcmatch_version(given) - - # verify. - assert result == expected + # wcswidth should work the same regardless of unicode_version value + assert wcwidth.wcswidth('hello', unicode_version='4.1.0') == 5 + assert wcwidth.wcswidth('hello', unicode_version='latest') == 5 + assert wcwidth.wcswidth('hello', unicode_version='invalid') == 5 |
