Files
bions-rust/bion-regex/tests/regex.rs
cloudion-labo 2556698dd3 🦀 bions-rust vague 1 : 6 briques build-your-own-x — on ne les rebuild plus jamais
Principe RS-7 : « dès qu'on build un truc, plus personne n'a à le rebuild —
la seule chose à faire est l'optimisation. » (nexus/RepoVerse)
- bion-vc      : horloges vectorielles + MvReg fork-visible (LA spec
                 xion-relativiste-v0 enfin codée — CRDT testé par permutations)
- bion-triplet : l'Adressage Génératif (gen_hash BLAKE3, coords, résidu ;
                 résidu vide quand déjà-su ; align décidable au bit)
- bion-tsoinlog: journal append-only rejouable (CRC32 maison, crash-recovery)
- bion-kv      : magasin clé-valeur bitcask (compaction atomique, tombstones)
- bion-regex   : moteur Thompson NFA linéaire (jamais exponentiel — Russ Cox)
- bion-git     : mini-git content-addressed (SHA-1 maison + vecteurs officiels,
                 branches divergentes = le fork visible)
129 tests verts, clippy 0 warning, doc française = chaque bion est un cours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:07:31 +00:00

284 lines
9.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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"));
}