🦀 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:
cloudion-labo
2026-08-16 01:07:31 +00:00
commit 2556698dd3
27 changed files with 6518 additions and 0 deletions

283
bion-regex/tests/regex.rs Normal file
View 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"));
}