//! Tests d'intégration de bion-regex : l'API publique, les pièges classiques //! et LA garantie du moteur — le motif pathologique reste instantané. //! //! NOTE : `clippy::invalid_regex` est un faux positif ici — le lint croit //! reconnaître le crate `regex` externe à cause du nom `Regex::new`, alors //! qu'on teste NOTRE moteur, y compris ses motifs volontairement invalides. #![allow(clippy::invalid_regex)] use bion_regex::{Error, Regex}; use std::time::{Duration, Instant}; // --------------------------------------------------------------------------- // Bases : littéraux, ., quantificateurs, alternance // --------------------------------------------------------------------------- #[test] fn litteraux() { let re = Regex::new("chat").unwrap(); assert!(re.is_match("le chat dort")); assert!(!re.is_match("le chien dort")); assert_eq!(re.pattern(), "chat"); } #[test] fn point_joker_sauf_newline() { let re = Regex::new("a.c").unwrap(); assert!(re.is_match("abc")); assert!(re.is_match("aéc")); // Unicode : é = 1 caractère (2 octets) assert!(!re.is_match("a\nc")); // `.` ne traverse pas les lignes assert!(!re.is_match("ac")); } #[test] fn quantificateurs_star_plus_quest() { let star = Regex::new("ab*c").unwrap(); assert!(star.is_match("ac")); assert!(star.is_match("abbbc")); let plus = Regex::new("ab+c").unwrap(); assert!(!plus.is_match("ac")); assert!(plus.is_match("abc")); let quest = Regex::new("ab?c").unwrap(); assert!(quest.is_match("ac")); assert!(quest.is_match("abc")); assert!(!quest.is_match("abbc")); } #[test] fn alternance() { let re = Regex::new("chat|chien|oiseau").unwrap(); assert!(re.is_match("un chien")); assert!(re.is_match("un oiseau")); assert!(!re.is_match("un poisson")); } // --------------------------------------------------------------------------- // Groupes (y compris imbriqués) // --------------------------------------------------------------------------- #[test] fn groupes_imbriques() { // ((ab)+c)|d — groupes dans groupes, quantifiés let re = Regex::new("((ab)+c)|d").unwrap(); assert!(re.is_match("abc")); assert!(re.is_match("abababc")); assert!(re.is_match("d")); assert!(!re.is_match("ac")); assert!(!re.is_match("ab")); } #[test] fn groupe_quantifie_et_alternance_interne() { let re = Regex::new("^(ab|cd)*$").unwrap(); assert!(re.is_match("")); assert!(re.is_match("abcdab")); assert!(!re.is_match("abc")); } // --------------------------------------------------------------------------- // Classes de caractères // --------------------------------------------------------------------------- #[test] fn classes_simples_et_intervalles() { let re = Regex::new("[a-f0-9]+").unwrap(); assert!(re.is_match("deadbeef42")); assert_eq!(re.find("xyz a3f xyz"), Some((4, 7))); assert!(!re.is_match("xyz")); } #[test] fn classe_negative() { let re = Regex::new("[^0-9 ]+").unwrap(); assert_eq!(re.find("12 abc 34"), Some((3, 6))); // « abc » let strict = Regex::new("^[^abc]+$").unwrap(); assert!(strict.is_match("xyz")); assert!(!strict.is_match("xbz")); // contient un 'b' } #[test] fn echappements_d_w_s() { let re = Regex::new(r"\d+\s\w+").unwrap(); assert!(re.is_match("il y a 42 bions")); assert!(!re.is_match("quarante-deux bions")); let nd = Regex::new(r"^\D+$").unwrap(); assert!(nd.is_match("abc!")); assert!(!nd.is_match("ab3c")); } #[test] fn metacaracteres_echappes() { let re = Regex::new(r"^\(\d+\.\d+\)$").unwrap(); assert!(re.is_match("(3.14)")); assert!(!re.is_match("(3x14)")); // le \. est bien littéral assert!(!re.is_match("3.14")); } // --------------------------------------------------------------------------- // Ancres // --------------------------------------------------------------------------- #[test] fn ancres_debut_fin() { let deb = Regex::new("^abc").unwrap(); assert!(deb.is_match("abcdef")); assert!(!deb.is_match("xabc")); let fin = Regex::new("abc$").unwrap(); assert!(fin.is_match("xxabc")); assert!(!fin.is_match("abcx")); let exact = Regex::new("^abc$").unwrap(); assert!(exact.is_match("abc")); assert!(!exact.is_match("aabc")); assert!(!exact.is_match("abcc")); } #[test] fn ancres_dans_les_branches() { // L'ancre est une assertion d'état, pas un simple préfixe : // elle marche aussi au milieu d'une alternance. let re = Regex::new("^début|fin$").unwrap(); assert!(re.is_match("début de texte")); assert!(re.is_match("texte fin")); assert!(!re.is_match("la fin arrive")); // « fin » pas en fin de texte assert!(!re.is_match("le début")); // « début » pas en début de texte } // --------------------------------------------------------------------------- // find : positions, leftmost-longest, matchs vides, Unicode // --------------------------------------------------------------------------- #[test] fn find_leftmost_longest() { let re = Regex::new("a+").unwrap(); assert_eq!(re.find("baaa"), Some((1, 4))); // le plus long à la 1re position assert_eq!(re.find("bbb"), None); // Leftmost prime sur longest : le match à 0 gagne même s'il est court. let re = Regex::new("ab?").unwrap(); assert_eq!(re.find("acabb"), Some((0, 1))); } #[test] fn find_match_vide() { // b* reconnaît la chaîne vide : match vide à la position 0. let re = Regex::new("b*").unwrap(); assert_eq!(re.find("aaab"), Some((0, 0))); assert_eq!(re.find(""), Some((0, 0))); } #[test] fn find_offsets_octets_unicode() { // Les offsets sont en OCTETS : « é » en occupe deux. let re = Regex::new("chè+vre").unwrap(); let texte = "la chèèèvre"; let (s, e) = re.find(texte).unwrap(); assert_eq!(&texte[s..e], "chèèèvre"); assert_eq!(s, 3); } #[test] fn texte_vide_et_motif_vide() { let vide = Regex::new("").unwrap(); assert!(vide.is_match("")); assert!(vide.is_match("abc")); assert_eq!(vide.find("abc"), Some((0, 0))); let re = Regex::new("a").unwrap(); assert!(!re.is_match("")); assert_eq!(re.find(""), None); } // --------------------------------------------------------------------------- // Erreurs de syntaxe propres // --------------------------------------------------------------------------- #[test] fn erreurs_de_syntaxe_propres() { assert_eq!( Regex::new("*a").unwrap_err(), Error::DanglingQuantifier { pos: 0 } ); assert_eq!( Regex::new("(ab").unwrap_err(), Error::UnbalancedParen { pos: 0 } ); assert_eq!( Regex::new("ab)").unwrap_err(), Error::UnbalancedParen { pos: 2 } ); assert_eq!( Regex::new("[abc").unwrap_err(), Error::UnclosedClass { pos: 0 } ); assert_eq!( Regex::new("[z-a]").unwrap_err(), Error::InvalidClassRange { pos: 1 } ); assert_eq!( Regex::new(r"\q").unwrap_err(), Error::UnknownEscape { c: 'q', pos: 1 } ); assert_eq!(Regex::new("ab\\").unwrap_err(), Error::TrailingBackslash); assert_eq!( Regex::new("^*").unwrap_err(), Error::QuantifierOnAnchor { pos: 1 } ); // Les messages Display sont en français et localisés : let msg = Regex::new("(ab").unwrap_err().to_string(); assert!(msg.contains("parenthèse"), "message inattendu : {msg}"); } // --------------------------------------------------------------------------- // LE test du crate : pas d'explosion exponentielle // --------------------------------------------------------------------------- #[test] fn pathologique_a_star_reste_instantane() { // « a*a*a*…a*b » sur 30 'a' sans 'b' : un backtracker doit essayer // ~2^30 découpages avant d'échouer. Le NFA de Thompson simulé par // ensembles d'états répond en O(texte × motif) — microsecondes. let pattern = format!("{}b", "a*".repeat(30)); let text = "a".repeat(30); let re = Regex::new(&pattern).unwrap(); let debut = Instant::now(); assert!(!re.is_match(&text)); assert_eq!(re.find(&text), None); let duree = debut.elapsed(); assert!( duree < Duration::from_secs(1), "pathologique trop lent : {duree:?} (devrait être ~µs)" ); } #[test] fn pathologique_a_quest_n_a_n() { // L'autre classique de l'article de Russ Cox : a?ⁿaⁿ sur « aⁿ ». // Ici le match EXISTE (tous les a? prennent vide) — un backtracker // le trouve vite dans ce sens, mais a?ⁿaⁿ sur aⁿ⁻¹ (échec) le tue. let n = 25; let pattern = format!("^{}{}$", "a?".repeat(n), "a".repeat(n)); let re = Regex::new(&pattern).unwrap(); let debut = Instant::now(); assert!(re.is_match(&"a".repeat(n))); // succès assert!(!re.is_match(&"a".repeat(n - 1))); // échec = le cas qui explose ailleurs let duree = debut.elapsed(); assert!(duree < Duration::from_secs(1), "trop lent : {duree:?}"); } #[test] fn boucles_vides_imbriquees() { // (a*)* et ((a*)*)* : cycles d'epsilon-transitions — la simulation doit // terminer (déduplication des états) et donner le bon résultat. let re = Regex::new("^((a*)*)*b$").unwrap(); assert!(re.is_match("b")); assert!(re.is_match("aaaab")); assert!(!re.is_match("aaaa")); }