Skip to content

Commit 2cf586d

Browse files
committed
Add proptests for search::query::glob_to_regex
Three properties on top of the existing 4 example tests: 1. `output_is_valid_anchored_regex`: for any arbitrary string, the produced regex compiles cleanly under `regex::Regex::new` and is anchored with `^...$`. This is the real safety net — the glob path feeds directly into a regex engine that panics on malformed input. 2. `literal_globs_match_themselves`: for globs without `*` or `?`, the compiled regex matches the original glob string and does NOT match a strictly larger string containing it. 3. `star_matches_arbitrary_content`: a `prefix*suffix` glob matches `prefix<any>suffix`. All three pass on the current implementation. No bugs surfaced — the metacharacter escape list in glob_to_regex is complete for the input space proptest explored.
1 parent e69e45a commit 2cf586d

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

  • apps/desktop/src-tauri/src/search

apps/desktop/src-tauri/src/search/query.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,88 @@ mod tests {
351351
assert_eq!(glob_to_regex("readme"), "^readme$");
352352
}
353353

354+
// ── glob_to_regex (property-based) ───────────────────────────────
355+
//
356+
// The output of `glob_to_regex` is fed directly into `regex::Regex::new`
357+
// by the search engine. A glob that escapes incorrectly would either
358+
// panic the regex parser or silently match more than the user intended.
359+
// These properties pin (a) the output is always a syntactically valid
360+
// regex and (b) it matches the user's literal intent when no glob
361+
// metacharacters are present.
362+
363+
mod glob_proptests {
364+
use super::*;
365+
use proptest::prelude::*;
366+
367+
proptest! {
368+
/// For any glob string, the produced regex compiles successfully
369+
/// and is anchored end-to-end.
370+
#[test]
371+
fn output_is_valid_anchored_regex(glob in ".*") {
372+
let pattern = glob_to_regex(&glob);
373+
prop_assert!(pattern.starts_with('^'), "regex must start with ^: {}", pattern);
374+
prop_assert!(pattern.ends_with('$'), "regex must end with $: {}", pattern);
375+
let compiled = regex::Regex::new(&pattern);
376+
prop_assert!(
377+
compiled.is_ok(),
378+
"regex must compile, got error for glob {:?}: {:?}",
379+
glob,
380+
compiled.err()
381+
);
382+
}
383+
384+
/// For globs with no `*` or `?` (after the regex metachar set is
385+
/// taken into account), the compiled regex matches the original
386+
/// string literally and nothing else of different content.
387+
#[test]
388+
fn literal_globs_match_themselves(
389+
glob in "[A-Za-z0-9 ._+(){}\\[\\]^$|\\\\]{0,30}"
390+
.prop_filter("no glob metacharacters", |s: &String| {
391+
!s.contains('*') && !s.contains('?')
392+
})
393+
) {
394+
let pattern = glob_to_regex(&glob);
395+
let compiled = regex::Regex::new(&pattern).expect("must compile");
396+
prop_assert!(
397+
compiled.is_match(&glob),
398+
"regex {:?} must match its own literal glob {:?}",
399+
pattern, glob
400+
);
401+
// It must not match a string with a different last character.
402+
// Skip strings ending in `]` or other edge codepoints because
403+
// appending arbitrary content might collide with grapheme
404+
// clusters in surprising ways — instead, prepend.
405+
let modified = format!("X{glob}Y");
406+
prop_assert!(
407+
!compiled.is_match(&modified) || modified == glob,
408+
"regex for literal glob {:?} must not match longer {:?}",
409+
glob, modified
410+
);
411+
}
412+
413+
/// For globs containing only `*` wildcards interleaved with
414+
/// literal segments, the compiled regex matches any string
415+
/// produced by replacing each `*` with the empty string OR an
416+
/// arbitrary literal segment.
417+
#[test]
418+
fn star_matches_arbitrary_content(
419+
prefix in "[A-Za-z0-9_]{0,5}",
420+
middle in "[A-Za-z0-9_]{0,10}",
421+
suffix in "[A-Za-z0-9_]{0,5}"
422+
) {
423+
let glob = format!("{prefix}*{suffix}");
424+
let pattern = glob_to_regex(&glob);
425+
let compiled = regex::Regex::new(&pattern).expect("must compile");
426+
let candidate = format!("{prefix}{middle}{suffix}");
427+
prop_assert!(
428+
compiled.is_match(&candidate),
429+
"regex {:?} for glob {:?} must match {:?}",
430+
pattern, glob, candidate
431+
);
432+
}
433+
}
434+
}
435+
354436
// ── summarize_query ──────────────────────────────────────────────
355437

356438
fn make_query(

0 commit comments

Comments
 (0)