use std::path::Path;
use cartog_db::Database;
use cartog_registry::{Declared, DeclaredUpdate, DescriptionSource, ProjectFacts};
pub fn record_indexed(db: &Database, db_path: &Path, root: &Path, declared: DeclaredUpdate) {
let facts = ProjectFacts {
last_indexed: Some(now_unix()),
declared,
..facts_with_counts(db, db_path, root)
};
cartog_registry::record_project(&facts);
}
pub fn record_embedded(db: &Database, db_path: &Path, root: &Path, declared: DeclaredUpdate) {
record_measured_without_indexing(db, db_path, root, declared);
}
pub fn record_backfilled(db: &Database, db_path: &Path, root: &Path, declared: DeclaredUpdate) {
record_measured_without_indexing(db, db_path, root, declared);
}
fn record_measured_without_indexing(
db: &Database,
db_path: &Path,
root: &Path,
declared: DeclaredUpdate,
) {
let facts = ProjectFacts {
declared,
..facts_with_counts(db, db_path, root)
};
cartog_registry::record_project(&facts);
}
pub fn record_opened(db_path: &Path, root: &Path) {
record_declared(db_path, root, DeclaredUpdate::Keep);
}
pub fn record_declared(db_path: &Path, root: &Path, declared: DeclaredUpdate) {
let mut facts = ProjectFacts {
declared,
..ProjectFacts::identity_only(db_path, root)
};
read_fingerprint_into(&mut facts, db_path);
cartog_registry::record_project(&facts);
}
#[must_use]
pub fn resolve_declared(name: Option<&str>, description: Option<&str>, root: &Path) -> Declared {
let description = match description {
Some(text) => Some(cartog_registry::Description {
text: text.to_string(),
source: DescriptionSource::Config,
}),
None => cartog_registry::readme_description(root),
};
Declared {
name: name.map(str::to_string),
description,
}
}
fn facts_with_counts(db: &Database, db_path: &Path, root: &Path) -> ProjectFacts {
let mut facts = ProjectFacts::identity_only(db_path, root);
if let Ok(stats) = db.stats() {
facts.file_count = Some(stats.num_files);
facts.symbol_count = Some(stats.num_symbols);
facts.edge_count = Some(stats.num_edges);
facts.resolved_count = Some(stats.num_resolved);
facts.languages = Some(stats.languages);
}
facts.embedding_count = db.embedding_count().ok();
read_fingerprint_into(&mut facts, db_path);
facts
}
fn read_fingerprint_into(facts: &mut ProjectFacts, db_path: &Path) {
let probed = cartog_db::read_database_facts_at(db_path);
facts.schema_version = probed.schema_version;
facts.embed_provider = probed.embed_provider;
facts.embed_model = probed.embed_model;
facts.embed_dim = probed.embed_dim;
}
fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs() as i64)
}
#[cfg(test)]
mod tests {
use super::*;
fn seeded_db(dir: &Path) -> (Database, std::path::PathBuf) {
let db_path = dir.join(".cartog").join("db.sqlite");
let db = Database::open(&db_path, 384).unwrap();
let sym = cartog_core::Symbol::new(
"foo",
cartog_core::SymbolKind::Function,
"a.rs",
1,
10,
0,
100,
None,
);
db.insert_symbols(std::slice::from_ref(&sym)).unwrap();
(db, db_path)
}
#[test]
fn indexed_facts_carry_counts_languages_and_a_last_indexed() {
let dir = tempfile::TempDir::new().unwrap();
let (db, db_path) = seeded_db(dir.path());
let facts = ProjectFacts {
last_indexed: Some(now_unix()),
..facts_with_counts(&db, &db_path, dir.path())
};
assert_eq!(facts.symbol_count, Some(1));
assert!(facts.file_count.is_some());
assert!(facts.languages.is_some());
assert!(
facts.last_indexed.is_some(),
"an index pass must stamp last_indexed, or the fingerprint skip could suppress it"
);
}
#[test]
fn opened_facts_carry_no_counts_so_they_cannot_clobber_an_index() {
let dir = tempfile::TempDir::new().unwrap();
let (_db, db_path) = seeded_db(dir.path());
let mut facts = ProjectFacts::identity_only(&db_path, dir.path());
read_fingerprint_into(&mut facts, &db_path);
assert_eq!(facts.symbol_count, None);
assert_eq!(facts.file_count, None);
assert_eq!(facts.languages, None);
assert_eq!(
facts.last_indexed, None,
"opening a project is not indexing it"
);
}
#[test]
fn the_schema_version_is_read_from_the_closed_file() {
let dir = tempfile::TempDir::new().unwrap();
let (db, db_path) = seeded_db(dir.path());
drop(db);
let mut facts = ProjectFacts::identity_only(&db_path, dir.path());
read_fingerprint_into(&mut facts, &db_path);
assert_eq!(
facts.schema_version,
Some(cartog_db::CURRENT_SCHEMA_VERSION)
);
}
#[test]
fn a_non_cartog_file_yields_no_schema_version_rather_than_zero() {
let dir = tempfile::TempDir::new().unwrap();
let foreign = dir.path().join("foreign.db");
std::fs::write(&foreign, b"not a database").unwrap();
let mut facts = ProjectFacts::identity_only(&foreign, dir.path());
read_fingerprint_into(&mut facts, &foreign);
assert_eq!(facts.schema_version, None);
}
#[test]
fn an_embedding_fingerprint_round_trips_from_the_closed_file() {
let dir = tempfile::TempDir::new().unwrap();
let (db, db_path) = seeded_db(dir.path());
db.reconcile_embedding_fingerprint(&cartog_db::EmbeddingFingerprint {
provider: "local".to_string(),
model: "bge-small".to_string(),
dimension: 384,
})
.unwrap();
drop(db);
let mut facts = ProjectFacts::identity_only(&db_path, dir.path());
read_fingerprint_into(&mut facts, &db_path);
assert_eq!(facts.embed_provider.as_deref(), Some("local"));
assert_eq!(facts.embed_model.as_deref(), Some("bge-small"));
assert_eq!(facts.embed_dim, Some(384));
}
#[test]
fn embedded_facts_stamp_no_last_indexed() {
let dir = tempfile::TempDir::new().unwrap();
let (db, db_path) = seeded_db(dir.path());
let mut facts = ProjectFacts::identity_only(&db_path, dir.path());
facts.embedding_count = db.embedding_count().ok();
assert_eq!(facts.last_indexed, None);
assert_eq!(facts.embedding_count, Some(0));
}
struct RegistryEnvGuard(Option<std::ffi::OsString>);
impl RegistryEnvGuard {
fn set(value: &std::ffi::OsStr) -> Self {
let prev = std::env::var_os(cartog_registry::REGISTRY_ENV);
std::env::set_var(cartog_registry::REGISTRY_ENV, value);
Self(prev)
}
}
impl Drop for RegistryEnvGuard {
fn drop(&mut self) {
match self.0.take() {
Some(v) => std::env::set_var(cartog_registry::REGISTRY_ENV, v),
None => std::env::remove_var(cartog_registry::REGISTRY_ENV),
}
}
}
#[test]
fn the_config_description_wins_over_the_readme() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("README.md"), "Inferred from the readme.\n").unwrap();
let declared = resolve_declared(
Some("billing-service"),
Some("Declared in config."),
dir.path(),
);
assert_eq!(declared.name.as_deref(), Some("billing-service"));
let d = declared.description.unwrap();
assert_eq!(d.text, "Declared in config.");
assert_eq!(d.source, DescriptionSource::Config);
}
#[test]
fn the_readme_is_used_when_the_config_declares_no_description() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join("README.md"),
"# Title\n\nInferred from the readme.\n",
)
.unwrap();
let declared = resolve_declared(Some("billing-service"), None, dir.path());
assert_eq!(declared.name.as_deref(), Some("billing-service"));
let d = declared.description.unwrap();
assert_eq!(d.text, "Inferred from the readme.");
assert_eq!(d.source, DescriptionSource::Readme);
}
#[test]
fn neither_source_yields_an_empty_declaration() {
let dir = tempfile::TempDir::new().unwrap();
assert_eq!(
resolve_declared(None, None, dir.path()),
Declared::default()
);
}
#[test]
fn a_config_without_a_name_leaves_the_declared_name_unset() {
let dir = tempfile::TempDir::new().unwrap();
let declared = resolve_declared(None, Some("Only a description."), dir.path());
assert_eq!(declared.name, None);
assert!(declared.description.is_some());
}
#[test]
#[serial_test::serial]
fn an_indexed_project_stores_its_declared_name_and_description() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
let declared = resolve_declared(
Some("billing-service"),
Some("Invoices and payments."),
dir.path(),
);
record_indexed(&db, &db_path, dir.path(), DeclaredUpdate::Set(declared));
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(row.declared_name.as_deref(), Some("billing-service"));
assert_eq!(row.display_name(), "billing-service");
let d = row.description.as_ref().expect("a stored description");
assert_eq!(d.text, "Invoices and payments.");
assert_eq!(d.source, DescriptionSource::Config);
}
#[test]
#[serial_test::serial]
fn an_embedding_pass_refreshes_the_declared_description_too() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
let declared = resolve_declared(None, Some("Embedded and described."), dir.path());
record_embedded(&db, &db_path, dir.path(), DeclaredUpdate::Set(declared));
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(
row.description.as_ref().map(|d| d.text.as_str()),
Some("Embedded and described.")
);
assert_eq!(row.last_indexed, None, "embedding is not an index pass");
}
#[test]
#[serial_test::serial]
fn opening_a_project_leaves_a_stored_description_intact() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
let declared = resolve_declared(
Some("billing-service"),
Some("Invoices and payments."),
dir.path(),
);
record_indexed(&db, &db_path, dir.path(), DeclaredUpdate::Set(declared));
record_opened(&db_path, dir.path());
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(row.declared_name.as_deref(), Some("billing-service"));
assert_eq!(
row.description.as_ref().map(|d| d.text.as_str()),
Some("Invoices and payments.")
);
}
#[test]
#[serial_test::serial]
fn removing_both_sources_clears_a_stored_description() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
let declared = resolve_declared(
Some("billing-service"),
Some("Invoices and payments."),
dir.path(),
);
record_indexed(&db, &db_path, dir.path(), DeclaredUpdate::Set(declared));
record_indexed(
&db,
&db_path,
dir.path(),
DeclaredUpdate::Set(resolve_declared(None, None, dir.path())),
);
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(row.declared_name, None);
assert_eq!(row.description, None);
}
#[test]
#[serial_test::serial]
fn a_config_edit_refreshes_the_description_on_a_no_op_pass() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
std::fs::write(dir.path().join("README.md"), "From the readme.\n").unwrap();
record_indexed(
&db,
&db_path,
dir.path(),
DeclaredUpdate::Set(resolve_declared(None, None, dir.path())),
);
record_declared(
&db_path,
dir.path(),
DeclaredUpdate::Set(resolve_declared(
Some("widget-service"),
Some("From the config."),
dir.path(),
)),
);
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(row.declared_name.as_deref(), Some("widget-service"));
let d = row.description.as_ref().expect("a stored description");
assert_eq!(d.text, "From the config.");
assert_eq!(d.source, DescriptionSource::Config);
}
#[test]
#[serial_test::serial]
fn a_declared_only_write_does_not_erase_the_counts_an_index_recorded() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
record_indexed(
&db,
&db_path,
dir.path(),
DeclaredUpdate::Set(Declared::default()),
);
record_declared(
&db_path,
dir.path(),
DeclaredUpdate::Set(resolve_declared(None, Some("Described later."), dir.path())),
);
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(row.symbol_count, Some(1), "counts must survive");
assert!(row.last_indexed.is_some(), "last_indexed must survive");
}
#[test]
#[serial_test::serial]
fn a_keep_write_leaves_a_stored_declared_identity_untouched() {
let dir = tempfile::TempDir::new().unwrap();
let registry = dir.path().join("reg.sqlite");
let _env = RegistryEnvGuard::set(registry.as_os_str());
let (db, db_path) = seeded_db(dir.path());
std::fs::write(dir.path().join("README.md"), "The readme fallback.\n").unwrap();
record_indexed(
&db,
&db_path,
dir.path(),
DeclaredUpdate::Set(resolve_declared(
Some("billing-service"),
Some("Invoices and payments."),
dir.path(),
)),
);
record_indexed(&db, &db_path, dir.path(), DeclaredUpdate::Keep);
let listing =
cartog_registry::list_projects_at(®istry, None, cartog_db::CURRENT_SCHEMA_VERSION);
let row = listing.projects.first().expect("the project was recorded");
assert_eq!(row.declared_name.as_deref(), Some("billing-service"));
let d = row.description.as_ref().expect("the stored description");
assert_eq!(d.text, "Invoices and payments.");
assert_eq!(d.source, DescriptionSource::Config);
}
#[test]
#[serial_test::serial]
fn recording_a_project_never_panics_when_the_registry_is_disabled() {
let dir = tempfile::TempDir::new().unwrap();
let (db, db_path) = seeded_db(dir.path());
let prev = std::env::var_os(cartog_registry::REGISTRY_ENV);
std::env::set_var(cartog_registry::REGISTRY_ENV, "");
record_indexed(
&db,
&db_path,
dir.path(),
DeclaredUpdate::Set(Declared::default()),
);
record_embedded(
&db,
&db_path,
dir.path(),
DeclaredUpdate::Set(Declared::default()),
);
record_declared(&db_path, dir.path(), DeclaredUpdate::Keep);
record_opened(&db_path, dir.path());
match prev {
Some(v) => std::env::set_var(cartog_registry::REGISTRY_ENV, v),
None => std::env::remove_var(cartog_registry::REGISTRY_ENV),
}
}
}