🦀 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>
This commit is contained in:
283
bion-regex/tests/regex.rs
Normal file
283
bion-regex/tests/regex.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
//! 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"));
|
||||
}
|
||||
Reference in New Issue
Block a user