youssefreda9 commited on
Commit
751ba66
·
1 Parent(s): 79407d3

test: add 28 fix verification tests (S2 S3 P1 hamza relabeling) — 59 total

Browse files
Files changed (1) hide show
  1. tests/test_pipeline_hardening.py +229 -0
tests/test_pipeline_hardening.py CHANGED
@@ -615,5 +615,234 @@ class TestApplyAllReverseSort(unittest.TestCase):
615
  self.assertNotEqual(forward_result, reverse_result)
616
 
617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  if __name__ == '__main__':
619
  unittest.main()
 
615
  self.assertNotEqual(forward_result, reverse_result)
616
 
617
 
618
+ # ═══════════════════════════════════════════════════════════════════════════════
619
+ # FIX VERIFICATION TESTS — Test that each model bug fix actually works
620
+ # ═══════════════════════════════════════════════════════════════════════════════
621
+
622
+ # Import the fix functions from app.py without starting Flask
623
+ def _import_app_functions():
624
+ """Import helper functions from app.py without loading Flask."""
625
+ import importlib.util
626
+ import types
627
+ app_path = os.path.join(os.path.dirname(__file__), '..', 'src', 'app.py')
628
+
629
+ # Read the file and extract just the functions we need
630
+ with open(app_path, 'r', encoding='utf-8') as f:
631
+ source = f.read()
632
+
633
+ # We need _levenshtein, _is_small_spelling_change,
634
+ # _is_spelling_only_change, _is_orthographic_variant
635
+ module = types.ModuleType('app_helpers')
636
+ module.__dict__['re'] = __import__('re')
637
+ # Extract function bodies
638
+ import re as _re
639
+
640
+ # Execute just the helper functions in an isolated namespace
641
+ func_names = [
642
+ '_levenshtein', '_is_small_spelling_change',
643
+ '_is_spelling_only_change', '_is_orthographic_variant'
644
+ ]
645
+ # Find and execute each function
646
+ for func_name in func_names:
647
+ pattern = rf'^(def {func_name}\(.*?\n(?:(?: .+\n|[ \t]*\n)*))'
648
+ match = _re.search(pattern, source, _re.MULTILINE)
649
+ if match:
650
+ exec(match.group(1), module.__dict__)
651
+
652
+ return module
653
+
654
+
655
+ class TestFixS2_GenderPreservation(unittest.TestCase):
656
+ """Fix S2: _is_small_spelling_change rejects corrections that drop feminine marker."""
657
+
658
+ @classmethod
659
+ def setUpClass(cls):
660
+ cls.helpers = _import_app_functions()
661
+
662
+ def test_rejects_baridah_to_barid(self):
663
+ """بارده→بارد drops feminine marker — must reject."""
664
+ result = self.helpers._is_small_spelling_change('بارده', 'بارد')
665
+ self.assertFalse(result, "Should reject correction that drops ه feminine ending")
666
+
667
+ def test_rejects_munkhafidah_to_munkhafid(self):
668
+ """منخفضه→منخفض drops feminine marker — must reject."""
669
+ result = self.helpers._is_small_spelling_change('منخفضه', 'منخفض')
670
+ self.assertFalse(result, "Should reject correction that drops ه feminine ending")
671
+
672
+ def test_accepts_ha_to_ta_marbuta(self):
673
+ """المدرسه→المدرسة is valid (ه→ة at end)."""
674
+ result = self.helpers._is_small_spelling_change('المدرسه', 'المدرسة')
675
+ self.assertTrue(result, "Should accept ه→ة correction")
676
+
677
+ def test_accepts_normal_spelling_fix(self):
678
+ """Normal spelling corrections still accepted."""
679
+ result = self.helpers._is_small_spelling_change('علىكم', 'عليكم')
680
+ self.assertTrue(result, "Should accept normal spelling correction")
681
+
682
+
683
+ class TestFixS3_HamzaWhitelist(unittest.TestCase):
684
+ """Fix S3: AraSpellPostProcessor.fix_common_hamza fixes common hamza errors."""
685
+
686
+ @classmethod
687
+ def setUpClass(cls):
688
+ from nlp.spelling.araspell_rules import AraSpellPostProcessor
689
+ cls.pp = AraSpellPostProcessor
690
+
691
+ def test_aliy_to_ila(self):
692
+ result = self.pp.fix_common_hamza('ذهبت الي المدرسة')
693
+ self.assertIn('إلى', result, "الي should become إلى")
694
+
695
+ def test_ant_to_anta(self):
696
+ result = self.pp.fix_common_hamza('هل انت ذاهب')
697
+ self.assertIn('أنت', result, "انت should become أنت")
698
+
699
+ def test_lan_to_lian(self):
700
+ result = self.pp.fix_common_hamza('غاب لان الطقس سيء')
701
+ self.assertIn('لأن', result, "لان should become لأن")
702
+
703
+ def test_ams_to_ams(self):
704
+ result = self.pp.fix_common_hamza('ذهبت امس')
705
+ self.assertIn('أمس', result, "امس should become أمس")
706
+
707
+ def test_alan_to_alan(self):
708
+ result = self.pp.fix_common_hamza('الان بدأ الدرس')
709
+ self.assertIn('الآن', result, "الان should become الآن")
710
+
711
+ def test_preserves_correct_words(self):
712
+ """Words not in whitelist should be unchanged."""
713
+ result = self.pp.fix_common_hamza('الكتاب جميل')
714
+ self.assertEqual(result, 'الكتاب جميل')
715
+
716
+
717
+ class TestPrefixedHamza(unittest.TestCase):
718
+ """Prefixed hamza: fix_common_hamza handles و/ف/ب/ك/ل + whitelist word."""
719
+
720
+ @classmethod
721
+ def setUpClass(cls):
722
+ from nlp.spelling.araspell_rules import AraSpellPostProcessor
723
+ cls.pp = AraSpellPostProcessor
724
+
725
+ def test_wa_asdiqai(self):
726
+ """واصدقائي → وأصدقائي"""
727
+ result = self.pp.fix_common_hamza('ذهبت واصدقائي')
728
+ self.assertIn('وأصدقائي', result, "واصدقائي should become وأصدقائي")
729
+
730
+ def test_fa_inna(self):
731
+ """فان → فأن"""
732
+ result = self.pp.fix_common_hamza('فان الامر واضح')
733
+ self.assertIn('فأن', result)
734
+
735
+ def test_ba_prefix(self):
736
+ """بامس not a valid lookup — should stay unchanged."""
737
+ # بامس is not ب+امس in a meaningful way, but the logic tries
738
+ text = 'مررت بامس'
739
+ result = self.pp.fix_common_hamza(text)
740
+ # ب + امس = ب + أمس → بأمس
741
+ self.assertIn('بأمس', result, "بامس should become بأمس")
742
+
743
+ def test_waw_an(self):
744
+ """وان → وأن"""
745
+ result = self.pp.fix_common_hamza('قال وان الحق واضح')
746
+ self.assertIn('وأن', result, "وان should become وأن")
747
+
748
+
749
+ class TestFixP1_PuncOnlyMarks(unittest.TestCase):
750
+ """Fix P1: PunctuationChecker._strip_non_punctuation_changes keeps only marks."""
751
+
752
+ def _make_checker(self):
753
+ """Create a minimal PunctuationChecker for testing strip logic."""
754
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
755
+ from nlp.punctuation.punctuation_service import PunctuationChecker
756
+ # Pass None for model/tokenizer/device since we only test the strip method
757
+ return PunctuationChecker(None, None, None)
758
+
759
+ def test_keeps_added_comma(self):
760
+ checker = self._make_checker()
761
+ original = 'ذهبت إلى المدرسة وكان الجو جميل'
762
+ punctuated = 'ذهبت إلى المدرسة، وكان الجو جميل.'
763
+ result = checker._strip_non_punctuation_changes(original, punctuated)
764
+ self.assertIn('،', result, "Should keep added comma")
765
+ self.assertIn('المدرسة', result)
766
+
767
+ def test_reverts_spelling_change(self):
768
+ checker = self._make_checker()
769
+ original = 'ذهبت الي المدرسه'
770
+ # Model changed الي→إلى AND المدرسه→المدرسة (spelling) + added period
771
+ punctuated = 'ذهبت إلى المدرسة.'
772
+ result = checker._strip_non_punctuation_changes(original, punctuated)
773
+ # Should revert spelling changes but keep the period
774
+ self.assertIn('الي', result, "Should revert الي (punc shouldn't fix spelling)")
775
+ self.assertIn('المدرسه', result, "Should revert المدرسه (punc shouldn't fix spelling)")
776
+
777
+ def test_identical_input_output(self):
778
+ checker = self._make_checker()
779
+ text = 'النص كما هو'
780
+ result = checker._strip_non_punctuation_changes(text, text)
781
+ self.assertEqual(result, text)
782
+
783
+
784
+ class TestGrammarRelabeling(unittest.TestCase):
785
+ """Grammar re-labeling: _is_spelling_only_change detects orthographic-only diffs."""
786
+
787
+ @classmethod
788
+ def setUpClass(cls):
789
+ cls.helpers = _import_app_functions()
790
+
791
+ def test_ha_to_ta_marbuta_is_spelling(self):
792
+ """المدرسه→المدرسة is a spelling fix, not grammar."""
793
+ result = self.helpers._is_spelling_only_change('المدرسه', 'المدرسة')
794
+ self.assertTrue(result, "ه→ة should be classified as spelling")
795
+
796
+ def test_hamza_alef_is_spelling(self):
797
+ """الي→إلى is a spelling fix."""
798
+ result = self.helpers._is_spelling_only_change('الي', 'إلى')
799
+ # الي→إلى has length difference and different chars
800
+ # This should be detected as orthographic
801
+ self.assertTrue(result, "hamza fix should be classified as spelling")
802
+
803
+ def test_verb_conjugation_is_grammar(self):
804
+ """سعيدون→سعيدين is a grammar fix, not spelling."""
805
+ result = self.helpers._is_spelling_only_change('سعيدون', 'سعيدين')
806
+ self.assertFalse(result, "ون→ين is grammar, not spelling")
807
+
808
+ def test_word_addition_is_grammar(self):
809
+ """Adding a word is grammar, not spelling."""
810
+ result = self.helpers._is_spelling_only_change('الطلاب ذهب', 'الطلاب ذهبوا')
811
+ self.assertFalse(result, "Word count change = grammar")
812
+
813
+ def test_anta_is_spelling(self):
814
+ """انت→أنت is a spelling fix."""
815
+ result = self.helpers._is_spelling_only_change('انت', 'أنت')
816
+ self.assertTrue(result, "hamza restoration should be classified as spelling")
817
+
818
+
819
+ class TestOrthographicVariant(unittest.TestCase):
820
+ """_is_orthographic_variant correctly classifies char-level differences."""
821
+
822
+ @classmethod
823
+ def setUpClass(cls):
824
+ cls.helpers = _import_app_functions()
825
+
826
+ def test_ha_ta_marbuta(self):
827
+ self.assertTrue(self.helpers._is_orthographic_variant('المدرسه', 'المدرسة'))
828
+
829
+ def test_alef_hamza(self):
830
+ self.assertTrue(self.helpers._is_orthographic_variant('انت', 'أنت'))
831
+
832
+ def test_ya_alef_maqsura(self):
833
+ self.assertTrue(self.helpers._is_orthographic_variant('الي', 'إلي'))
834
+
835
+ def test_completely_different_words(self):
836
+ self.assertFalse(self.helpers._is_orthographic_variant('كتاب', 'مدرسة'))
837
+
838
+ def test_grammar_change_not_spelling(self):
839
+ """ون→ين is grammar, not orthographic variant."""
840
+ self.assertFalse(self.helpers._is_orthographic_variant('سعيدون', 'سعيدين'))
841
+
842
+ def test_identical_words(self):
843
+ """Identical words are NOT orthographic variants (no diff)."""
844
+ self.assertFalse(self.helpers._is_orthographic_variant('كتاب', 'كتاب'))
845
+
846
+
847
  if __name__ == '__main__':
848
  unittest.main()