1+ # SPDX-FileCopyrightText: 2026 PyThaiNLP Project
2+ # SPDX-FileType: SOURCE
3+ # SPDX-License-Identifier: Apache-2.0
4+ """
5+ Robustness tests for PyThaiNLP functions.
6+
7+ This test suite focuses on edge cases important for real-world usage:
8+ - Empty strings and various whitespace handling
9+ - Special characters from terminal copy/paste and encoding issues
10+ - Unicode edge cases (truncated, BOM, control characters)
11+ - Emoji and hidden/invisible characters
12+ - Multi-engine robustness testing for word_tokenize
13+ - Very long strings that can cause performance issues (issue #893)
14+ """
15+
16+ import unittest
17+
18+ from pythainlp .tokenize import word_tokenize
19+
20+
21+ class RobustnessTestCase (unittest .TestCase ):
22+ """Test PyThaiNLP functions with edge cases."""
23+
24+ # Tokenization engines to test (core engines without external dependencies)
25+ TOKENIZE_ENGINES = ["newmm" , "newmm-safe" , "longest" , "mm" ]
26+
27+ # Category: Empty and Whitespace strings
28+ # Real-world cases from copy/paste, terminal input, etc.
29+ EMPTY_AND_WHITESPACE = [
30+ "" , # Empty string
31+ " " , # Single space
32+ " " , # Multiple spaces
33+ "\t " , # Tab
34+ "\n " , # Newline
35+ "\r \n " , # Windows line ending
36+ " \t \n \r " , # Mixed whitespace
37+ "\u00a0 " , # Non-breaking space
38+ "\u2000 \u2001 \u2002 " , # Various unicode spaces
39+ "\u3000 " , # Ideographic space (CJK)
40+ ]
41+
42+ # Category: Special characters from encoding issues and terminal
43+ # BOM, control characters, special punctuation
44+ SPECIAL_CHARS = [
45+ "\ufeff " , # BOM (Byte Order Mark)
46+ "\ufffe " , # BOM reversed
47+ "\x00 " , # Null character
48+ "\u0000 " , # Null unicode
49+ "" , # Zero-width non-joiner, zero-width joiner
50+ "\u200c \u200d " , # ZWNJ, ZWJ
51+ "\u200e \u200f " , # LTR mark, RTL mark
52+ "" , # Soft hyphen
53+ "\u00ad " , # Soft hyphen unicode
54+ "…" , # Ellipsis
55+ "—–-" , # Different dashes
56+ "\u201c \u201d " , # Smart double quotes (curly quotes)
57+ "\u2018 \u2019 " , # Smart single quotes (curly quotes)
58+ ]
59+
60+ # Category: Truncated/malformed Unicode
61+ # Characters that might be cut in the middle of encoding
62+ TRUNCATED_UNICODE = [
63+ "\ud800 " , # High surrogate alone (invalid)
64+ "\udc00 " , # Low surrogate alone (invalid)
65+ "test\ud800 text" , # High surrogate in middle
66+ "สวัสดี\udc00 " , # Thai with low surrogate
67+ ]
68+
69+ # Category: Emoji and Modern Unicode
70+ # Emoji, emoji sequences, and variations
71+ EMOJI_CASES = [
72+ "😀" , # Basic emoji
73+ "👨👩👧👦" , # Family emoji (ZWJ sequence)
74+ "👍🏻" , # Emoji with skin tone modifier
75+ "🇹🇭" , # Flag (regional indicator symbols)
76+ "😀😃😄" , # Multiple emoji
77+ "สวัสดี😀ครับ" , # Thai text with emoji
78+ "🏴" , # Flag with tag sequences
79+ ]
80+
81+ # Category: Control and Hidden Characters
82+ # Characters that don't display but affect processing
83+ CONTROL_AND_HIDDEN = [
84+ "\x01 \x02 \x03 " , # Control characters
85+ "\u0001 \u0002 \u0003 " , # Control chars unicode
86+ "\u200b " , # Zero-width space
87+ "\u200c " , # Zero-width non-joiner
88+ "\u200d " , # Zero-width joiner
89+ "\ufeff " , # Zero-width no-break space (BOM)
90+ "test\u200b word" , # Text with zero-width space
91+ "ภาษา\u200c ไทย" , # Thai with ZWNJ
92+ "\u034f " , # Combining grapheme joiner
93+ ]
94+
95+ # Category: Thai-specific edge cases with combining characters
96+ THAI_EDGE_CASES = [
97+ "ก่ก้ก๊ก๋" , # Multiple tone marks
98+ "ด้้้้้็็็็็" , # Excessive combining marks
99+ "ไทย123English" , # Mixed scripts
100+ "ๆๆๆ" , # Repetition marks
101+ "\u200b \u200b \u200b " , # Zero-width spaces
102+ ]
103+
104+ # Category: Very Long Strings (issue #893)
105+ VERY_LONG_STRINGS = [
106+ "ชิ" * 50 , # Repetitive single syllable
107+ "ด้านหน้า" * 20 , # Repetitive compound word
108+ "ด้านหน้า" * 10 + "กกกกกก" * 10 , # Mixed patterns
109+ ]
110+
111+ def test_word_tokenize_empty_and_whitespace (self ):
112+ """
113+ Test word_tokenize with empty strings and various whitespace.
114+
115+ These cases are common in real-world usage from copy/paste,
116+ terminal input, and various text processing scenarios.
117+ """
118+ for engine in self .TOKENIZE_ENGINES :
119+ for s in self .EMPTY_AND_WHITESPACE :
120+ with self .subTest (engine = engine , input_string = repr (s )):
121+ try :
122+ result = word_tokenize (s , engine = engine )
123+ self .assertIsInstance (result , list )
124+ except Exception as e :
125+ self .fail (
126+ f"word_tokenize (engine={ engine } ) failed with "
127+ f"whitespace case { repr (s )} : { e } "
128+ )
129+
130+ def test_word_tokenize_special_chars (self ):
131+ """
132+ Test word_tokenize with special characters.
133+
134+ Tests BOM, control characters, zero-width characters, and special
135+ punctuation that often appear from encoding issues or terminal copy/paste.
136+ """
137+ for engine in self .TOKENIZE_ENGINES :
138+ for s in self .SPECIAL_CHARS :
139+ with self .subTest (engine = engine , input_string = repr (s )):
140+ try :
141+ result = word_tokenize (s , engine = engine )
142+ self .assertIsInstance (result , list )
143+ except Exception as e :
144+ self .fail (
145+ f"word_tokenize (engine={ engine } ) failed with "
146+ f"special char { repr (s )} : { e } "
147+ )
148+
149+ def test_word_tokenize_truncated_unicode (self ):
150+ """
151+ Test word_tokenize with truncated or malformed Unicode.
152+
153+ Tests handling of surrogate pairs and characters that might be
154+ cut in the middle of multi-byte encoding.
155+ """
156+ for engine in self .TOKENIZE_ENGINES :
157+ for s in self .TRUNCATED_UNICODE :
158+ with self .subTest (engine = engine , input_string = repr (s )):
159+ try :
160+ result = word_tokenize (s , engine = engine )
161+ self .assertIsInstance (result , list )
162+ except Exception as e :
163+ # Truncated unicode might cause issues, but shouldn't crash
164+ self .fail (
165+ f"word_tokenize (engine={ engine } ) failed with "
166+ f"truncated unicode { repr (s )} : { e } "
167+ )
168+
169+ def test_word_tokenize_emoji (self ):
170+ """
171+ Test word_tokenize with emoji and modern Unicode sequences.
172+
173+ Tests emoji, emoji with modifiers, ZWJ sequences, and flags.
174+ Important for modern text processing.
175+ """
176+ for engine in self .TOKENIZE_ENGINES :
177+ for s in self .EMOJI_CASES :
178+ with self .subTest (engine = engine , input_string = repr (s )):
179+ try :
180+ result = word_tokenize (s , engine = engine )
181+ self .assertIsInstance (result , list )
182+ if s .strip ():
183+ self .assertGreater (len (result ), 0 )
184+ except Exception as e :
185+ self .fail (
186+ f"word_tokenize (engine={ engine } ) failed with "
187+ f"emoji { repr (s )} : { e } "
188+ )
189+
190+ def test_word_tokenize_control_and_hidden (self ):
191+ """
192+ Test word_tokenize with control and hidden characters.
193+
194+ Tests zero-width characters, control characters, and other
195+ invisible characters that affect text processing.
196+ """
197+ for engine in self .TOKENIZE_ENGINES :
198+ for s in self .CONTROL_AND_HIDDEN :
199+ with self .subTest (engine = engine , input_string = repr (s )):
200+ try :
201+ result = word_tokenize (s , engine = engine )
202+ self .assertIsInstance (result , list )
203+ except Exception as e :
204+ self .fail (
205+ f"word_tokenize (engine={ engine } ) failed with "
206+ f"control/hidden char { repr (s )} : { e } "
207+ )
208+
209+ def test_word_tokenize_thai_edge_cases_multi_engine (self ):
210+ """
211+ Test word_tokenize with Thai edge cases across multiple engines.
212+
213+ Tests complex Thai character combinations including excessive combining
214+ marks, mixed scripts, and special characters across all core engines
215+ to ensure consistent handling.
216+ """
217+ for engine in self .TOKENIZE_ENGINES :
218+ for s in self .THAI_EDGE_CASES :
219+ with self .subTest (engine = engine , input_string = repr (s )):
220+ try :
221+ result = word_tokenize (s , engine = engine )
222+ self .assertIsInstance (result , list )
223+ # Non-empty input should produce at least one token
224+ if s .strip ():
225+ self .assertGreater (len (result ), 0 )
226+ except Exception as e :
227+ self .fail (
228+ f"word_tokenize (engine={ engine } ) failed with "
229+ f"Thai edge case { repr (s )} : { e } "
230+ )
231+
232+ def test_word_tokenize_with_very_long_strings (self ):
233+ """
234+ Test word_tokenize with very long strings.
235+
236+ This test addresses issue #893 - testing tokenization performance
237+ and robustness with very long strings, including repetitive patterns
238+ that can cause ambiguous breaking points and long processing times.
239+
240+ Uses newmm-safe engine which is designed to handle such cases.
241+ """
242+ # Use newmm-safe which has safeguards against long processing times
243+ engine = "newmm-safe"
244+ for i , s in enumerate (self .VERY_LONG_STRINGS ):
245+ with self .subTest (engine = engine , string_index = i ):
246+ try :
247+ result = word_tokenize (s , engine = engine )
248+ self .assertIsInstance (result , list )
249+ # Very long strings should still produce tokens
250+ self .assertGreater (len (result ), 0 )
251+ except Exception as e :
252+ self .fail (
253+ f"word_tokenize (engine={ engine } ) failed with "
254+ f"very long string (index={ i } ): { e } "
255+ )
256+
0 commit comments