feat: add persistent CPU search engine and CLI with lifecycle tests
Core CI / core (ubuntu-latest) (push) Successful in 1m0s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Successful in 1m0s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
aae803771c
commit
f77112db97
19 files changed
+1688
No files matched your search
@@ -0,0 +1,136 @@
|
||||
use anyhow::Result;
|
||||
use searchi_core::{IndexOptions, SearchIndex, SearchOptions};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn exclusions_database_and_unchanged_metadata() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
for directory in [".git", "node_modules", "target", "private"] {
|
||||
fs::create_dir(root.path().join(directory))?;
|
||||
fs::write(root.path().join(directory).join("hidden.txt"), "secret")?;
|
||||
}
|
||||
fs::write(root.path().join("visible.txt"), "original content")?;
|
||||
let mut index = SearchIndex::open(root.path().join("index.db"))?;
|
||||
let options = IndexOptions {
|
||||
excluded_directory_names: vec![
|
||||
".git".into(),
|
||||
"node_modules".into(),
|
||||
"target".into(),
|
||||
"private".into(),
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
index
|
||||
.index_folder_with_options(root.path(), &options)?
|
||||
.indexed,
|
||||
1
|
||||
);
|
||||
assert_eq!(index.stats()?.files, 1);
|
||||
let unchanged = index.index_folder_with_options(root.path(), &options)?;
|
||||
assert_eq!(unchanged.unchanged, 1);
|
||||
assert_eq!(unchanged.indexed, 0);
|
||||
assert!(index.search("secret", 10)?.is_empty());
|
||||
assert!(index.search("index.db", 10)?.is_empty());
|
||||
fs::write(
|
||||
root.path().join("visible.txt"),
|
||||
"changed and longer content",
|
||||
)?;
|
||||
assert_eq!(
|
||||
index
|
||||
.index_folder_with_options(root.path(), &options)?
|
||||
.indexed,
|
||||
1
|
||||
);
|
||||
assert!(index.search("original", 10)?.is_empty());
|
||||
assert_eq!(index.search("changed", 10)?.len(), 1);
|
||||
// A new exclusion removes previously indexed entries from this root.
|
||||
let mut options = IndexOptions::default();
|
||||
index.index_folder_with_options(root.path(), &options)?;
|
||||
assert_eq!(index.search("secret", 10)?.len(), 1);
|
||||
options.excluded_directory_names.push("private".into());
|
||||
assert_eq!(
|
||||
index
|
||||
.index_folder_with_options(root.path(), &options)?
|
||||
.removed,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_apply_before_limit_and_respect_folder_boundaries() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let docs = root.path().join("docs");
|
||||
let sibling = root.path().join("docs-more");
|
||||
fs::create_dir(&docs)?;
|
||||
fs::create_dir(&sibling)?;
|
||||
fs::write(docs.join("shared.md"), "shared short")?;
|
||||
fs::write(docs.join("shared.txt"), "shared considerably longer")?;
|
||||
fs::write(sibling.join("shared.md"), "shared sibling")?;
|
||||
let mut index = SearchIndex::open(":memory:")?;
|
||||
index.index_folder(root.path())?;
|
||||
let options = SearchOptions {
|
||||
folder: Some(docs),
|
||||
extension: Some(".MD".into()),
|
||||
max_size: Some(15),
|
||||
..Default::default()
|
||||
};
|
||||
let results = index.search_with_options("shared", 1, &options)?;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].filename, "shared.md");
|
||||
assert!(!results[0].path.contains("docs-more"));
|
||||
let options = SearchOptions {
|
||||
min_size: Some(20),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(index.search_with_options("shared", 10, &options)?.len(), 1);
|
||||
let options = SearchOptions {
|
||||
modified_before_unix_ms: Some(0),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(index
|
||||
.search_with_options("shared", 10, &options)?
|
||||
.is_empty());
|
||||
let options = SearchOptions {
|
||||
min_size: Some(20),
|
||||
max_size: Some(10),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(index.search_with_options("shared", 10, &options).is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrates_v1_database_without_losing_search_records() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let db = root.path().join("legacy.db");
|
||||
let connection = rusqlite::Connection::open(&db)?;
|
||||
connection.execute_batch(
|
||||
"CREATE TABLE files(id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE,
|
||||
root TEXT NOT NULL, filename TEXT NOT NULL, content TEXT NOT NULL);
|
||||
PRAGMA user_version=1;",
|
||||
)?;
|
||||
drop(connection);
|
||||
let mut index = SearchIndex::open(&db)?;
|
||||
fs::write(root.path().join("sample.txt"), "migration evidence")?;
|
||||
index.index_folder(root.path())?;
|
||||
assert_eq!(index.search("evidence", 10)?.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readers_open_and_search_during_uncommitted_writer() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let db = root.path().join("index.db");
|
||||
fs::write(root.path().join("notes.txt"), "committed evidence")?;
|
||||
let mut index = SearchIndex::open(&db)?;
|
||||
index.index_folder(root.path())?;
|
||||
let writer = rusqlite::Connection::open(&db)?;
|
||||
writer.execute_batch("BEGIN IMMEDIATE; UPDATE files SET content='uncommitted replacement';")?;
|
||||
// Opening a current schema must not issue DDL or acquire a write lock.
|
||||
let reader = SearchIndex::open(&db)?;
|
||||
assert_eq!(reader.search("evidence", 10)?.len(), 1);
|
||||
assert!(reader.search("replacement", 10)?.is_empty());
|
||||
writer.execute_batch("ROLLBACK;")?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user