diff --git a/tests/core/test_morpheme.py b/tests/core/test_morpheme.py index 273670c57..5f9e1fe31 100644 --- a/tests/core/test_morpheme.py +++ b/tests/core/test_morpheme.py @@ -15,6 +15,10 @@ def test_nighit(self): self.assertEqual(nighit("สํ", "นิษฐาน"), "สันนิษฐาน") self.assertEqual(nighit("สํ", "ปทา"), "สัมปทา") self.assertEqual(nighit("สํ", "โยค"), "สังโยค") + with self.assertRaises(NotImplementedError): + nighit("abc", "คีต") # w1 does not end with ํ and len > 2 + with self.assertRaises(NotImplementedError): + nighit("สํ", "มาร") # consonant ม is not in any supported group def test_is_native_thai(self): self.assertFalse(is_native_thai(None), False) diff --git a/tests/core/test_security.py b/tests/core/test_security.py index 07095b942..9cc6f79a0 100644 --- a/tests/core/test_security.py +++ b/tests/core/test_security.py @@ -135,6 +135,23 @@ def test_safe_extract_zip_rejects_path_traversal(self): _safe_extract_zip(zf, extract_dir) self.assertIn("path traversal", str(context.exception).lower()) + def test_safe_extract_zip_rejects_symlink_escape(self): + """Test that safe zip extraction rejects zip symlinks pointing outside.""" + with tempfile.TemporaryDirectory() as tmpdir: + zip_path = os.path.join(tmpdir, "symlink_attack.zip") + with zipfile.ZipFile(zip_path, "w") as zf: + info = zipfile.ZipInfo("evil_symlink") + # Set Unix symlink mode: S_IFLNK = 0o120000 + info.external_attr = 0o120777 << 16 + zf.writestr(info, "../../etc/passwd") + + extract_dir = os.path.join(tmpdir, "extract") + os.makedirs(extract_dir) + with zipfile.ZipFile(zip_path, "r") as zf: + with self.assertRaises(ValueError) as context: + _safe_extract_zip(zf, extract_dir) + self.assertIn("symlink", str(context.exception).lower()) + def test_safe_extract_tar_rejects_symlink_escape(self): """Test that safe tar extraction rejects symlinks pointing outside.""" with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/core/test_tag.py b/tests/core/test_tag.py index 51ccba01f..98b6e4370 100644 --- a/tests/core/test_tag.py +++ b/tests/core/test_tag.py @@ -95,6 +95,12 @@ def test_pos_tag(self): ], ) + def test_pos_tag_error_handling(self): + with self.assertRaises(ValueError): + pos_tag(["ทดสอบ"], engine="invalid_engine", corpus="invalid_corpus") + with self.assertRaises(ValueError): + pos_tag(["ทดสอบ"], engine="unigram", corpus="invalid_corpus") + def test_NER_error_handling(self): with self.assertRaises(ValueError): NER(engine="xx_non_existing", corpus="thainer") diff --git a/tests/core/test_util.py b/tests/core/test_util.py index a5ba83938..ae7ec0ecd 100644 --- a/tests/core/test_util.py +++ b/tests/core/test_util.py @@ -9,6 +9,7 @@ import unittest from collections import Counter from datetime import date, datetime, time, timedelta, timezone +from unittest.mock import patch from pythainlp.corpus import corpus_path, thai_words from pythainlp.util import ( @@ -61,6 +62,7 @@ thai_consonant_to_spelling, thai_digit_to_arabic_digit, thai_keyboard_dist, + thai_lunar_date, thai_strftime, thai_strptime, thai_to_eng, @@ -1017,10 +1019,18 @@ def test_remove_repeat_consonants(self): def test_morse_encode(self): self.assertEqual(morse_encode("แมว", lang="th"), ".-.- -- .--") self.assertEqual(morse_encode("cat", lang="en"), "-.-. .- -") + with self.assertRaisesRegex( + NotImplementedError, "This function doesn't support jp" + ): + morse_encode("แมว", lang="jp") def test_morse_decode(self): self.assertEqual(morse_decode(".-.- -- .--", lang="th"), "แมว") self.assertEqual(morse_decode("-.-. .- -", lang="en"), "CAT") + with self.assertRaisesRegex( + NotImplementedError, "This function doesn't support jp" + ): + morse_decode(".-.- -- .--", lang="jp") def test_to_lunar_date(self): self.assertEqual(to_lunar_date(date(2024, 11, 15)), "ขึ้น 15 ค่ำ เดือน 12") @@ -1030,6 +1040,11 @@ def test_to_lunar_date(self): self.assertEqual(to_lunar_date(date(2020, 10, 31)), "ขึ้น 15 ค่ำ เดือน 12") with self.assertRaises(NotImplementedError): to_lunar_date(date(1885, 9, 7)) # back to the future + with patch.object(thai_lunar_date, "last_day_in_year", return_value=353): + with self.assertRaisesRegex( + ValueError, "Unexpected last_day value: 353" + ): + to_lunar_date(date(1903, 1, 1)) def test_th_zodiac(self): self.assertEqual(th_zodiac(2024), "มะโรง") diff --git a/tests/extra/testx_transliterate.py b/tests/extra/testx_transliterate.py index 2db5004b1..438b0fce3 100644 --- a/tests/extra/testx_transliterate.py +++ b/tests/extra/testx_transliterate.py @@ -7,7 +7,15 @@ import torch from pythainlp.corpus import remove -from pythainlp.transliterate import pronunciate, puan, romanize, transliterate +from pythainlp.transliterate import ( + lookup, + pronunciate, + puan, + romanize, + thai2rom, + thaig2p, + transliterate, +) from pythainlp.transliterate.ipa import trans_list, xsampa_list from pythainlp.transliterate.thai2rom import ThaiTransliterator from pythainlp.transliterate.thai2rom_onnx import ThaiTransliterator_ONNX @@ -101,6 +109,59 @@ def test_thai2rom_prepare_sequence(self): .tolist(), ) + def test_thai2rom_unsupported_attention_method_raises_value_error(self): + attn = thai2rom.Attn(method="unsupported", hidden_size=4) + hidden = torch.randn(1, 1, 4) + encoder_outputs = torch.randn(1, 2, 4) + mask = torch.ones(1, 2, dtype=torch.bool) + + with self.assertRaisesRegex(ValueError, "Unsupported attention method"): + attn(hidden, encoder_outputs, mask) + + def test_thai2rom_seq2seq_hidden_mismatch_raises_value_error(self): + encoder = thai2rom.Encoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=8, + dropout=0.0, + ) + decoder = thai2rom.AttentionDecoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=10, + dropout=0.0, + ) + + with self.assertRaisesRegex( + ValueError, "Encoder and decoder hidden sizes must match" + ): + thai2rom.Seq2Seq(encoder, decoder, 2, 3, 10) + + def test_thai2rom_seq2seq_inference_teacher_forcing_raises_value_error(self): + encoder = thai2rom.Encoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=8, + dropout=0.0, + ) + decoder = thai2rom.AttentionDecoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=8, + dropout=0.0, + ) + network = thai2rom.Seq2Seq(encoder, decoder, 2, 3, 10) + + with self.assertRaisesRegex( + ValueError, "teacher_forcing_ratio must be zero during inference" + ): + network( + torch.tensor([[1, 2, 0]], dtype=torch.long), + torch.tensor([2], dtype=torch.int), + None, + teacher_forcing_ratio=0.5, + ) + def test_thai2rom_onnx_prepare_sequence(self): transliterater = ThaiTransliterator_ONNX() @@ -134,6 +195,65 @@ def test_thai2rom_onnx_prepare_sequence(self): .tolist(), ) + def test_thaig2p_unsupported_attention_method_raises_value_error(self): + attn = thaig2p.Attn(method="unsupported", hidden_size=4) + hidden = torch.randn(1, 1, 4) + encoder_outputs = torch.randn(1, 2, 4) + mask = torch.ones(1, 2, dtype=torch.bool) + + with self.assertRaisesRegex(ValueError, "Unsupported attention method"): + attn(hidden, encoder_outputs, mask) + + def test_thaig2p_seq2seq_hidden_mismatch_raises_value_error(self): + encoder = thaig2p.Encoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=8, + dropout=0.0, + ) + decoder = thaig2p.AttentionDecoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=10, + dropout=0.0, + ) + + with self.assertRaisesRegex( + ValueError, "Encoder and decoder hidden sizes must match" + ): + thaig2p.Seq2Seq(encoder, decoder, 2, 3, 10) + + def test_thaig2p_seq2seq_inference_teacher_forcing_raises_value_error(self): + encoder = thaig2p.Encoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=8, + dropout=0.0, + ) + decoder = thaig2p.AttentionDecoder( + vocabulary_size=16, + embedding_size=8, + hidden_size=8, + dropout=0.0, + ) + network = thaig2p.Seq2Seq(encoder, decoder, 2, 3, 10) + + with self.assertRaisesRegex( + ValueError, "teacher_forcing_ratio must be zero during inference" + ): + network( + torch.tensor([[1, 2, 0]], dtype=torch.long), + [2], + None, + teacher_forcing_ratio=0.5, + ) + + def test_lookup_non_callable_fallback_raises_type_error(self): + with self.assertRaisesRegex( + TypeError, "`fallback_engine` is not callable" + ): + lookup.romanize("___", fallback_func="not-callable") # type: ignore[arg-type] + def test_transliterate(self): self.assertEqual(transliterate("แมว", "pyicu"), "mæw") self.assertEqual(transliterate("คน", engine="ipa"), "kʰon")