@@ -142,4 +142,114 @@ public function test_analyze_metrics_are_consistent(): void
142142 $ this ->assertGreaterThan (0 , $ stats ->letterCount );
143143 $ this ->assertGreaterThan (0 , $ stats ->sentenceCount );
144144 }
145+
146+ // --- Property-based tests (invariant checks across many inputs) ---
147+
148+ public function test_fuzz_syllable_count_in_bounds (): void
149+ {
150+ $ engine = new Engine ('en-us ' , self ::DATA_DIR . '/patterns ' , self ::DATA_DIR . '/languages ' );
151+
152+ $ words = [
153+ '' , 'a ' , 'i ' , 'the ' , 'cat ' , 'dog ' , 'run ' , 'go ' , 'be ' , 'no ' ,
154+ 'table ' , 'apple ' , 'house ' , 'water ' , 'light ' , 'world ' , 'phone ' ,
155+ 'banana ' , 'computer ' , 'elephant ' , 'umbrella ' , 'beautiful ' , 'tomorrow ' ,
156+ 'information ' , 'university ' , 'extraordinary ' , 'communicate ' , 'relationship ' ,
157+ 'revolutionary ' , 'internationalization ' , 'uncharacteristically ' ,
158+ 'antidisestablishmentarianism ' , 'pneumonoultramicroscopicsilicovolcanoconiosis ' ,
159+ ];
160+
161+ foreach ($ words as $ word ) {
162+ $ wordLength = \mb_strlen ($ word );
163+ $ count = $ engine ->syllableCount ($ word );
164+
165+ if ($ wordLength === 0 ) {
166+ $ this ->assertSame (0 , $ count , "Empty word must have 0 syllables " );
167+ } else {
168+ $ this ->assertGreaterThanOrEqual (1 , $ count , "Word ' {$ word }' has {$ count } syllables (expected >=1) " );
169+ $ this ->assertLessThanOrEqual ($ wordLength , $ count , "Word ' {$ word }' has {$ count } syllables (expected <= {$ wordLength }) " );
170+ }
171+ }
172+ }
173+
174+ public function test_fuzz_split_word_reconstructs_original (): void
175+ {
176+ $ engine = new Engine ('en-us ' , self ::DATA_DIR . '/patterns ' , self ::DATA_DIR . '/languages ' );
177+
178+ $ words = [
179+ 'a ' , 'the ' , 'cat ' , 'dog ' , 'table ' , 'apple ' , 'house ' , 'water ' , 'light ' ,
180+ 'banana ' , 'computer ' , 'elephant ' , 'umbrella ' , 'beautiful ' , 'tomorrow ' ,
181+ 'information ' , 'university ' , 'extraordinary ' , 'communicate ' ,
182+ 'revolutionary ' , 'international ' , 'circumstance ' , 'accommodation ' ,
183+ ];
184+
185+ foreach ($ words as $ word ) {
186+ $ parts = $ engine ->splitWord ($ word );
187+ $ reconstructed = \implode ('' , $ parts );
188+ $ this ->assertSame ($ word , $ reconstructed , "Word ' {$ word }' not reconstructed from parts: " . \implode ('- ' , $ parts ));
189+ $ this ->assertCount ($ engine ->syllableCount ($ word ), $ parts , "Word ' {$ word }' syllable count mismatch " );
190+ }
191+ }
192+
193+ public function test_fuzz_consistent_across_repeated_calls (): void
194+ {
195+ $ engine = new Engine ('en-us ' , self ::DATA_DIR . '/patterns ' , self ::DATA_DIR . '/languages ' );
196+
197+ $ words = [
198+ 'the ' , 'cat ' , 'table ' , 'banana ' , 'computer ' , 'elephant ' , 'beautiful ' ,
199+ 'university ' , 'extraordinary ' , 'communication ' ,
200+ ];
201+
202+ for ($ i = 0 ; $ i < 5 ; $ i ++) {
203+ foreach ($ words as $ word ) {
204+ $ parts1 = $ engine ->splitWord ($ word );
205+ $ parts2 = $ engine ->splitWord ($ word );
206+ $ this ->assertSame ($ parts1 , $ parts2 , "Word ' {$ word }' split inconsistently on iteration {$ i }" );
207+ $ this ->assertSame ($ engine ->syllableCount ($ word ), \count ($ parts1 ), "Word ' {$ word }' count mismatch on iteration {$ i }" );
208+ }
209+ }
210+ }
211+
212+ public function test_fuzz_empty_and_edge_cases (): void
213+ {
214+ $ engine = new Engine ('en-us ' , self ::DATA_DIR . '/patterns ' , self ::DATA_DIR . '/languages ' );
215+
216+ $ this ->assertSame (0 , $ engine ->syllableCount ('' ));
217+ $ this ->assertSame ([], $ engine ->splitWord ('' ));
218+
219+ $ singleChars = ['a ' , 'b ' , 'c ' , 'z ' , 'A ' , 'Z ' ];
220+ foreach ($ singleChars as $ char ) {
221+ $ this ->assertSame (1 , $ engine ->syllableCount ($ char ), "Char ' {$ char }' " );
222+ $ this ->assertSame ([$ char ], $ engine ->splitWord ($ char ), "Char ' {$ char }' split " );
223+ }
224+ }
225+
226+ public function test_fuzz_multilingual_invariants (): void
227+ {
228+ $ langWords = [
229+ 'en-us ' => ['the ' , 'table ' , 'banana ' , 'computer ' , 'university ' ],
230+ 'ru ' => ['а ' , 'мы ' , 'слово ' , 'молоко ' , 'красивый ' ],
231+ 'de-1996 ' => ['in ' , 'und ' , 'Schreiben ' , 'Verständlichkeit ' ],
232+ 'fr ' => ['le ' , 'bonjour ' , 'ordinateur ' ],
233+ 'es ' => ['y ' , 'hola ' , 'hermosa ' , 'computadora ' ],
234+ 'it ' => ['e ' , 'ciao ' , 'bellissimo ' , 'università ' ],
235+ 'nl ' => ['de ' , 'goed ' , 'morgen ' ],
236+ 'pt ' => ['de ' , 'obrigado ' , 'computador ' ],
237+ 'tr ' => ['ve ' , 'merhaba ' , 'güzel ' ],
238+ 'pl ' => ['i ' , 'dzień ' , 'piękny ' ],
239+ ];
240+
241+ foreach ($ langWords as $ lang => $ words ) {
242+ $ engine = new Engine ($ lang , self ::DATA_DIR . '/patterns ' , self ::DATA_DIR . '/languages ' );
243+ foreach ($ words as $ word ) {
244+ $ parts = $ engine ->splitWord ($ word );
245+ $ reconstructed = \implode ('' , $ parts );
246+ $ this ->assertSame ($ word , $ reconstructed , "Lang {$ lang }: ' {$ word }' not reconstructed " );
247+
248+ $ count = $ engine ->syllableCount ($ word );
249+ $ this ->assertGreaterThanOrEqual (1 , $ count , "Lang {$ lang }: ' {$ word }' has 0 syllables " );
250+ $ this ->assertLessThanOrEqual (\mb_strlen ($ word ), $ count , "Lang {$ lang }: ' {$ word }' has too many syllables " );
251+ $ this ->assertCount ($ count , $ parts , "Lang {$ lang }: ' {$ word }' count != parts " );
252+ }
253+ }
254+ }
145255}
0 commit comments