|
| 1 | +"""Tests for deterministic output capture text chunking.""" |
| 2 | + |
| 3 | +import time |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from sqlbuild.runtime.output_capture._helpers.text import chunk_text |
| 8 | +from tests.unit.src.sqlbuild.runtime.output_capture._helpers._test_types import ( |
| 9 | + ChunkTextErrorTestCase, |
| 10 | + ChunkTextFunction, |
| 11 | + ChunkTextOracleTestCase, |
| 12 | + ChunkTextPerformanceTestCase, |
| 13 | + ChunkTextTestCase, |
| 14 | +) |
| 15 | +from tests.unit.src.sqlbuild.runtime.output_capture._helpers.helpers import ( |
| 16 | + random_mixed_width_texts, |
| 17 | +) |
| 18 | + |
| 19 | + |
| 20 | +@pytest.mark.parametrize( |
| 21 | + "test_case", |
| 22 | + ( |
| 23 | + ChunkTextTestCase("two_byte_straddles_limit", "aé", 2, ("a", "é")), |
| 24 | + ChunkTextTestCase("three_byte_straddles_limit", "ab订c", 4, ("ab", "订c")), |
| 25 | + ChunkTextTestCase("four_byte_straddles_limit", "abc📦", 4, ("abc", "📦")), |
| 26 | + ChunkTextTestCase("four_byte_exact_limit", "📦📦", 4, ("📦", "📦")), |
| 27 | + ChunkTextTestCase("combining_mark_split_from_base", "ée\u0301", 4, ("ée", "\u0301")), |
| 28 | + ChunkTextTestCase("character_wider_than_limit", "a📦b", 2, ("a", "📦", "b")), |
| 29 | + ChunkTextTestCase("each_character_when_limit_zero", "a订", 0, ("a", "订")), |
| 30 | + ChunkTextTestCase("empty_text_negative_limit", "", -1, ("",)), |
| 31 | + ChunkTextTestCase("escaped_surrogate_counts_one_byte", "é\udc80a", 3, ("é\udc80", "a")), |
| 32 | + ), |
| 33 | + ids=lambda case: case.description, |
| 34 | +) |
| 35 | +def test_given_multibyte_boundaries_when_chunking_then_code_points_stay_whole( |
| 36 | + test_case: ChunkTextTestCase, |
| 37 | + reference_chunk_text: ChunkTextFunction, |
| 38 | +) -> None: |
| 39 | + result: tuple[str, ...] = chunk_text(text=test_case.text, max_bytes=test_case.max_bytes) |
| 40 | + |
| 41 | + assert result == test_case.expected_chunks |
| 42 | + assert result == reference_chunk_text(text=test_case.text, max_bytes=test_case.max_bytes) |
| 43 | + |
| 44 | + |
| 45 | +@pytest.mark.parametrize( |
| 46 | + "test_case", |
| 47 | + (ChunkTextErrorTestCase("unescapable_surrogate", "é\ud800", 2, UnicodeEncodeError),), |
| 48 | + ids=lambda case: case.description, |
| 49 | +) |
| 50 | +def test_given_unescapable_surrogate_when_chunking_then_raises_like_reference( |
| 51 | + test_case: ChunkTextErrorTestCase, |
| 52 | + reference_chunk_text: ChunkTextFunction, |
| 53 | +) -> None: |
| 54 | + with pytest.raises(test_case.expected_error): |
| 55 | + _ = reference_chunk_text(text=test_case.text, max_bytes=test_case.max_bytes) |
| 56 | + with pytest.raises(test_case.expected_error): |
| 57 | + _ = chunk_text(text=test_case.text, max_bytes=test_case.max_bytes) |
| 58 | + |
| 59 | + |
| 60 | +@pytest.mark.parametrize( |
| 61 | + "test_case", |
| 62 | + [ |
| 63 | + ChunkTextOracleTestCase( |
| 64 | + f"limit_{max_bytes}", |
| 65 | + seed=max_bytes, |
| 66 | + max_bytes=max_bytes, |
| 67 | + count=400, |
| 68 | + expected_mismatch_count=0, |
| 69 | + ) |
| 70 | + for max_bytes in (1, 2, 3, 4, 5, 6, 7, 8, 13, 64, 257) |
| 71 | + ], |
| 72 | + ids=lambda case: case.description, |
| 73 | +) |
| 74 | +def test_given_random_mixed_width_text_when_chunking_then_matches_reference( |
| 75 | + test_case: ChunkTextOracleTestCase, |
| 76 | + reference_chunk_text: ChunkTextFunction, |
| 77 | +) -> None: |
| 78 | + texts: tuple[str, ...] = random_mixed_width_texts( |
| 79 | + seed=test_case.seed, max_bytes=test_case.max_bytes, count=test_case.count |
| 80 | + ) |
| 81 | + |
| 82 | + actual: tuple[tuple[str, ...], ...] = tuple( |
| 83 | + chunk_text(text=text, max_bytes=test_case.max_bytes) for text in texts |
| 84 | + ) |
| 85 | + |
| 86 | + expected: tuple[tuple[str, ...], ...] = tuple( |
| 87 | + reference_chunk_text(text=text, max_bytes=test_case.max_bytes) for text in texts |
| 88 | + ) |
| 89 | + mismatch_count: int = sum( |
| 90 | + result != reference for result, reference in zip(actual, expected, strict=True) |
| 91 | + ) |
| 92 | + assert mismatch_count == test_case.expected_mismatch_count |
| 93 | + assert tuple("".join(chunks) for chunks in actual) == texts |
| 94 | + |
| 95 | + |
| 96 | +@pytest.mark.parametrize( |
| 97 | + "test_case", |
| 98 | + ( |
| 99 | + ChunkTextPerformanceTestCase( |
| 100 | + "non_ascii_ten_megabytes", |
| 101 | + unit="order ünïcode 订单 📦 e\u0301 ", |
| 102 | + payload_bytes=10_000_000, |
| 103 | + max_bytes=64 * 1024, |
| 104 | + expected_max_seconds=0.1, |
| 105 | + ), |
| 106 | + ), |
| 107 | + ids=lambda case: case.description, |
| 108 | +) |
| 109 | +def test_given_large_non_ascii_payload_when_chunking_then_completes_within_budget( |
| 110 | + test_case: ChunkTextPerformanceTestCase, |
| 111 | +) -> None: |
| 112 | + unit_bytes: int = len(test_case.unit.encode("utf-8")) |
| 113 | + text: str = test_case.unit * (test_case.payload_bytes // unit_bytes + 1) |
| 114 | + timings: list[float] = [] |
| 115 | + result: tuple[str, ...] = () |
| 116 | + |
| 117 | + for _ in range(3): |
| 118 | + started: float = time.perf_counter() |
| 119 | + result = chunk_text(text=text, max_bytes=test_case.max_bytes) |
| 120 | + timings.append(time.perf_counter() - started) |
| 121 | + |
| 122 | + assert "".join(result) == text |
| 123 | + assert all(len(chunk.encode("utf-8")) <= test_case.max_bytes for chunk in result) |
| 124 | + assert min(timings) < test_case.expected_max_seconds |
| 125 | + |
| 126 | + |
| 127 | +if __name__ == "__main__": |
| 128 | + pytest.main([__file__, "-vv"]) |
0 commit comments