quick_search/crates/quicksearch-core/src/scope_tests.rs

301 lines
9.8 KiB
Rust
Raw Normal View History

use super::*;
use crate::walk::{walk_indexable_files, WalkEvent};
use std::collections::HashSet;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
fn tmp_tree(tag: &str) -> PathBuf {
crate::testutil::scratch_dir_canonical(tag)
}
fn touch(p: &Path) {
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, b"x").unwrap();
}
fn empty_db(dir: &Path) -> PathBuf {
let db = dir.join("index.sqlite");
crate::db::open_or_recreate(db.to_str().unwrap(), "trigram").unwrap();
db
}
/// Every file the walker actually emits under `config`'s single root.
fn walked(config: &Config, db: &Path) -> HashSet<PathBuf> {
let root = config.paths.indexing_paths[0].clone();
walk_indexable_files(
&[root],
config.indexing.follow_symlinks,
config.indexing.include_hidden,
IgnoreSet::compile(&config.indexing.ignore_patterns).unwrap(),
db.to_str().unwrap(),
config.clone(),
Arc::new(Registry::default_set()),
Arc::new(AtomicBool::new(false)),
2,
)
.filter_map(|e| match e {
WalkEvent::File(f) => Some(PathBuf::from(f.path)),
WalkEvent::Stale(_) => None,
})
.collect()
}
/// One `files` row per path, which is all the scan reads.
fn seed(conn: &mut Connection, paths: &[PathBuf]) {
let tx = conn.transaction().unwrap();
for path in paths {
let path = path.to_string_lossy();
let (parent, name) = path.rsplit_once('/').unwrap();
repo::insert_file(
&tx,
&repo::NewFile {
name,
path: &path,
parent,
size: 1,
mtime: 1,
mime: Some("text/plain"),
ftype: crate::mime::FileType::TEXT,
hash: None,
needs_content: false,
},
)
.unwrap()
.expect("unique path");
}
tx.commit().unwrap();
}
/// Every file that physically exists under `root`, walker or no walker.
fn on_disk(root: &Path) -> Vec<PathBuf> {
walkdir::WalkDir::new(root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_type().is_file())
.map(|e| e.into_path())
.collect()
}
/// `Scope` must reach the same verdict the walker does for every file on
/// disk: stricter, and every prune deletes rows the next run puts straight
/// back; laxer, and the rows the user excluded survive.
#[test]
fn scope_agrees_with_the_walker() {
let root = tmp_tree("agree");
touch(&root.join("keep.txt"));
touch(&root.join("sub/keep2.txt"));
touch(&root.join("sub/skip.tmp"));
touch(&root.join("sub/node_modules/dep/index.js"));
touch(&root.join(".hidden/inside.txt"));
touch(&root.join(".dotfile"));
touch(&root.join("build/out/artifact.o"));
touch(&root.join("build/keep3.txt"));
touch(&root.join("nested/build/also.o"));
let mut config = Config::default();
config.paths.indexing_paths = vec![root.to_string_lossy().into_owned()];
config.indexing.ignore_patterns = vec![
"*.tmp".into(),
"node_modules".into(),
// A full-path pattern: prunes this one directory, not every
// directory called `out`.
root.join("build/out").to_string_lossy().into_owned(),
];
for include_hidden in [false, true] {
config.indexing.include_hidden = include_hidden;
let db = empty_db(&tmp_tree("agree-db"));
let emitted = walked(&config, &db);
let scope = Scope::from_config(&config).unwrap();
for path in on_disk(&root) {
assert_eq!(
scope.covers(&root, &path),
emitted.contains(&path),
"disagreement on {} (include_hidden = {})",
path.display(),
include_hidden
);
}
}
std::fs::remove_dir_all(&root).ok();
}
/// A root is never filtered — the user chose it. A component pattern naming
/// the root must not empty it out, but a full-path pattern matching the
/// root still prunes everything below it.
#[test]
fn a_root_is_never_filtered_but_its_children_still_are() {
let base = tmp_tree("root-name");
let root = base.join("node_modules");
touch(&root.join("keep.txt"));
touch(&root.join("node_modules/nested.txt"));
let mut config = Config::default();
config.paths.indexing_paths = vec![root.to_string_lossy().into_owned()];
config.indexing.ignore_patterns = vec!["node_modules".into()];
let scope = Scope::from_config(&config).unwrap();
assert!(scope.covers(&root, &root.join("keep.txt")));
assert!(!scope.covers(&root, &root.join("node_modules/nested.txt")));
let db = empty_db(&tmp_tree("root-name-db"));
let emitted = walked(&config, &db);
for path in on_disk(&root) {
assert_eq!(scope.covers(&root, &path), emitted.contains(&path));
}
// A full-path pattern reaching the root itself takes the whole tree.
config.indexing.ignore_patterns = vec![root.to_string_lossy().into_owned()];
let scope = Scope::from_config(&config).unwrap();
assert!(!scope.covers(&root, &root.join("keep.txt")));
std::fs::remove_dir_all(&base).ok();
}
/// The counters the status display reads: a scan that reports nothing is
/// indistinguishable from a hang.
#[test]
fn the_scan_reports_its_way_through_every_row() {
let root = tmp_tree("progress");
for i in 0..7 {
touch(&root.join(format!("f{}.log", i)));
}
touch(&root.join("keep.txt"));
let db_dir = tmp_tree("progress-db");
let db = empty_db(&db_dir);
let mut conn = crate::db::open_existing(db.to_str().unwrap(), true).unwrap();
let mut config = Config::default();
config.paths.indexing_paths = vec![root.to_string_lossy().into_owned()];
// One row per page, so a counter that only moved at the end is visible.
config.processing.batch_size = 1;
seed(&mut conn, &on_disk(&root));
let mut narrowed = config.clone();
narrowed.indexing.ignore_patterns = vec!["*.log".into()];
let work = crate::config::diff_actions(&config, &narrowed).work;
let mut cursor = WorkCursor::new(work, &narrowed).unwrap();
assert_eq!(
cursor.progress(),
ReconcileProgress::default(),
"nothing counted before the first slice"
);
let registry = Registry::default_set();
let run = AtomicBool::new(false);
let mut seen: Vec<usize> = Vec::new();
while !cursor.done() {
// A deadline already past, so each call does the least it can and
// the counters are sampled at their finest granularity.
advance(
&mut conn,
&narrowed,
&registry,
&mut cursor,
Instant::now(),
&run,
)
.unwrap();
seen.push(cursor.progress().examined);
}
let end = cursor.progress();
assert_eq!(end.total, Some(8), "counted once, before the first page");
assert_eq!(end.examined, 8, "every row was re-tested");
assert_eq!(end.deleted, 7, "the logs, and only the logs");
assert!(
seen.windows(2).all(|w| w[0] <= w[1]),
"the count never goes backwards: {:?}",
seen
);
assert!(
seen.len() > 2 && seen[0] < end.examined,
"progress was reported during the scan, not only at its end: {:?}",
seen
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&db_dir).ok();
}
/// Cancelling stops the pass at the next statement boundary and leaves the
/// cursor un-finished, so nothing downstream can record the configuration
/// as reconciled. Rows already reached stay gone — the pass is idempotent
/// and the next run finishes it.
#[test]
fn cancelling_stops_the_scan_without_finishing_it() {
let root = tmp_tree("cancel");
for i in 0..6 {
touch(&root.join(format!("f{}.log", i)));
}
touch(&root.join("keep.txt"));
let db_dir = tmp_tree("cancel-db");
let db = empty_db(&db_dir);
let mut conn = crate::db::open_existing(db.to_str().unwrap(), true).unwrap();
let mut config = Config::default();
config.paths.indexing_paths = vec![root.to_string_lossy().into_owned()];
config.processing.batch_size = 1;
seed(&mut conn, &on_disk(&root));
let mut narrowed = config.clone();
narrowed.indexing.ignore_patterns = vec!["*.log".into()];
let work = crate::config::diff_actions(&config, &narrowed).work;
let registry = Registry::default_set();
// Cancelled from the outset: not one statement runs.
let stop = AtomicBool::new(true);
let mut cursor = WorkCursor::new(work.clone(), &narrowed).unwrap();
advance(
&mut conn,
&narrowed,
&registry,
&mut cursor,
Instant::now() + SLICE,
&stop,
)
.unwrap();
assert!(!cursor.done(), "a cancelled pass is never finished");
assert_eq!(
cursor.progress(),
ReconcileProgress::default(),
"a cancelled pass touched the index"
);
// And part-way through: one slice with the flag clear, the rest with
// it set. The counters keep what the first slice earned.
let stop = AtomicBool::new(false);
let mut cursor = WorkCursor::new(work, &narrowed).unwrap();
advance(
&mut conn,
&narrowed,
&registry,
&mut cursor,
Instant::now(),
&stop,
)
.unwrap();
let part_way = cursor.progress();
assert!(part_way.examined > 0 && !cursor.done(), "nothing to cancel");
stop.store(true, Ordering::Relaxed);
advance(
&mut conn,
&narrowed,
&registry,
&mut cursor,
Instant::now() + SLICE,
&stop,
)
.unwrap();
assert!(!cursor.done(), "the pass finished despite the cancellation");
assert_eq!(
cursor.progress(),
part_way,
"the cancelled slice did more work"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&db_dir).ok();
}