use std::thread;
use terraphim_automata::{
CompiledMatcher, Matched, MatcherBuilder, MatcherOptions, find_matches, thesaurus_from_terms,
};
use terraphim_types::{NormalizedTerm, NormalizedTermValue, RoleName, Thesaurus};
fn thesaurus_sample() -> Thesaurus {
let role = RoleName::new("Engineer");
thesaurus_from_terms(
&role,
[
"rust",
"rust async",
"async runtime",
"knowledge graph",
"machine learning",
],
)
}
#[test]
fn compiled_matches_equal_find_matches() {
let thesaurus = thesaurus_sample();
let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();
let text = "Rust and RUST ASYNC power the knowledge graph with an async runtime.";
let expected = find_matches(text, &thesaurus, true).unwrap();
let got = compiled.find_matches(text, true).unwrap();
assert_eq!(got, expected);
}
#[test]
fn repeated_reuse_is_identical() {
let thesaurus = thesaurus_sample();
let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();
let first = compiled
.find_matches("rust knowledge graph machine learning", false)
.unwrap()
.len();
assert_eq!(first, 3);
for _ in 0..50 {
let matches = compiled
.find_matches("rust knowledge graph machine learning", false)
.unwrap();
assert_eq!(matches.len(), first);
}
}
#[test]
fn builder_skips_invalid_patterns() {
let mut builder = MatcherBuilder::new(MatcherOptions::default());
let _ = builder.insert("rust".to_string(), sample_term("rust"));
let short = builder.insert("a".to_string(), sample_term("a")).is_err();
let empty = builder.insert(" ".to_string(), sample_term(" ")).is_err();
assert!(short, "too-short pattern must be rejected");
assert!(empty, "blank pattern must be rejected");
let compiled = builder.build().unwrap();
let matches = compiled.find_matches("rust", true).unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].term, "rust");
}
#[test]
fn builder_rejects_duplicate_patterns() {
let mut builder = MatcherBuilder::new(MatcherOptions::default());
builder
.insert("rust".to_string(), sample_term("rust"))
.unwrap();
let dup = builder.insert("rust".to_string(), sample_term("rust"));
assert!(dup.is_err(), "duplicate patterns must be rejected");
}
#[test]
fn empty_pattern_set_builds_and_matches_nothing() {
let builder = MatcherBuilder::new(MatcherOptions::default());
let compiled = builder.build().unwrap();
assert!(compiled.is_empty());
assert!(compiled.find_matches("rust", true).unwrap().is_empty());
}
#[test]
fn empty_query_returns_no_matches() {
let compiled =
CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
assert!(compiled.find_matches("", true).unwrap().is_empty());
}
#[test]
fn leftmost_longest_semantics_preserved() {
let compiled =
CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
let matches = compiled.find_matches("rust async", true).unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].term, "rust async");
}
#[test]
fn multibyte_unicode_positions_are_byte_accurate() {
let mut builder = MatcherBuilder::new(MatcherOptions::default());
builder
.insert("héllo".to_string(), sample_term("héllo"))
.unwrap();
let compiled = builder.build().unwrap();
let text = "say héllo wörld";
let expected = {
let role = RoleName::new("T");
let t = thesaurus_from_terms(&role, ["héllo"]);
find_matches(text, &t, true).unwrap()
};
let got = compiled.find_matches(text, true).unwrap();
assert_eq!(got.len(), expected.len());
assert_eq!(got[0].term, expected[0].term);
assert_eq!(got[0].pos, expected[0].pos);
let (start, end) = got[0].pos.unwrap();
assert_eq!(&text[start..end], "héllo");
}
#[test]
fn overlapping_matches_reported_like_reference() {
let role = RoleName::new("T");
let thesaurus = thesaurus_from_terms(&role, ["abc", "bcd"]);
let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();
let got = compiled.find_matches("abcd", true).unwrap();
let expected = find_matches("abcd", &thesaurus, true).unwrap();
assert_eq!(got, expected);
assert!(got.is_empty(), "a term inside a longer word is not a match");
let got = compiled.find_matches("abc bcd", true).unwrap();
let expected = find_matches("abc bcd", &thesaurus, true).unwrap();
assert_eq!(got, expected);
assert_eq!(got.len(), 2, "both terms match as standalone words");
}
#[test]
fn concurrent_read_only_use_is_safe() {
let compiled =
CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
let handles: Vec<_> = (0..8)
.map(|i| {
let compiled = compiled.clone();
thread::spawn(move || {
let text = format!("rust async knowledge graph number {i}");
let matches = compiled.find_matches(&text, true).unwrap();
assert_eq!(matches.len(), 2);
matches
})
})
.collect();
for h in handles {
let matches = h.join().unwrap();
assert_eq!(matches[0].term, "rust async");
assert_eq!(matches[1].term, "knowledge graph");
}
}
#[test]
fn caller_buffer_path_avoids_reallocation() {
let compiled =
CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
let mut buffer: Vec<Matched> = Vec::new();
compiled.push_matches("rust async", &mut buffer).unwrap();
assert!(!buffer.is_empty());
buffer.clear();
compiled.push_matches("nothing here", &mut buffer).unwrap();
assert!(buffer.is_empty());
assert!(buffer.capacity() >= 1, "buffer reuse is preserved");
}
#[test]
fn options_are_serialisable_construction_config() {
let options = MatcherOptions::default();
let json = serde_json::to_string(&options).unwrap();
let back: MatcherOptions = serde_json::from_str(&json).unwrap();
assert_eq!(options, back);
}
#[test]
fn min_pattern_length_option_is_enforced() {
let options = MatcherOptions {
min_pattern_length: 5,
..MatcherOptions::default()
};
let mut builder = MatcherBuilder::new(options);
let rejected = builder.insert("rust".to_string(), sample_term("rust"));
assert!(rejected.is_err());
let accepted = builder.insert("runtime".to_string(), sample_term("runtime"));
assert!(accepted.is_ok());
}
#[test]
fn case_insensitive_default_matches_reference() {
let role = RoleName::new("T");
let thesaurus = thesaurus_from_terms(&role, ["rust"]);
let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();
let got = compiled.find_matches("RUST rust Rust", true).unwrap();
assert_eq!(got.len(), 3);
}
fn sample_term(value: &str) -> NormalizedTerm {
NormalizedTerm::new(1u64, NormalizedTermValue::from(value))
}