use std::collections::BTreeSet;
use std::sync::Arc;
use super::*;
fn source(file: usize, language: &str, path: &str) -> SourceFileRecord {
SourceFileRecord {
id: SourceId::at(file),
uri: format!("code+moniker://./file:{path}"),
source_root: 0,
path: path.to_string(),
rel_path: path.to_string(),
anchor: path.to_string(),
language: language.to_string(),
text: String::new(),
}
}
fn symbol(file: usize, def: usize, identity: &str, name: &str) -> SymbolRecord {
let mut symbol = SymbolRecord::new(SymbolId::at(file, def), SourceId::at(file), name, "class");
symbol.identity = Arc::from(identity);
symbol.line_range = Some((3, 3));
symbol
}
#[test]
fn indexes_facets_and_preserves_sparse_universe_complements() {
let sources = vec![
source(0, "java", "src/main/java/acme/infra/Good.java"),
source(1, "java", "src/main/java/acme/domain/Bad.java"),
];
let symbols = RecordTable::from_shards(vec![
Arc::from(vec![symbol(
0,
0,
"code+moniker://./lang:java/srcset:main/package:acme/dir:infra/class:GoodRepository",
"GoodRepository",
)]),
Arc::from(vec![symbol(
1,
0,
"code+moniker://./lang:java/srcset:main/package:acme/dir:domain/class:BadRepository",
"BadRepository",
)]),
]);
let inventory = SymbolInventoryIndex::build(ResourceGeneration::new(7), &sources, &symbols);
assert_eq!(inventory.all_symbols().len(), 2);
assert_eq!(
inventory
.facets()
.symbols_by_segment("dir", "infra")
.expect("infra posting")
.len(),
1
);
assert_eq!(
inventory
.facets()
.symbols_by_srcset("main")
.expect("srcset posting")
.len(),
2
);
let infra = inventory
.facets()
.symbols_by_segment("dir", "infra")
.expect("infra posting");
let outside = inventory.all_symbols().difference(infra);
assert_eq!(outside.len(), 1);
}
#[test]
fn refresh_keeps_identity_ordinal_and_retires_removed_symbols() {
let sources = vec![source(0, "java", "src/main/java/acme/Order.java")];
let identity = "code+moniker://./lang:java/srcset:main/package:acme/class:OrderRepository";
let before = RecordTable::from_shards(vec![Arc::from(vec![symbol(
0,
0,
identity,
"OrderRepository",
)])]);
let inventory = SymbolInventoryIndex::build(ResourceGeneration::new(1), &sources, &before);
let ordinal = inventory
.facets()
.symbols_by_identity(identity)
.and_then(|symbols| symbols.iter().next())
.expect("initial ordinal");
let after = RecordTable::from_shards(vec![Arc::from(vec![symbol(
0,
1,
identity,
"OrderRepository",
)])]);
let refreshed = inventory.refresh(
ResourceGeneration::new(2),
&sources,
&after,
&BTreeSet::from([0]),
);
assert!(
refreshed
.facets()
.symbols_by_identity(identity)
.is_some_and(|symbols| symbols.contains(ordinal))
);
assert_eq!(refreshed.catalog().id(ordinal), Some(&SymbolId::at(0, 1)));
assert_eq!(refreshed.all_symbols().len(), 1);
let empty = RecordTable::from_shards(vec![Arc::from(Vec::<SymbolRecord>::new())]);
let removed = refreshed.refresh(
ResourceGeneration::new(3),
&sources,
&empty,
&BTreeSet::from([0]),
);
assert!(removed.facets().symbols_by_identity(identity).is_none());
assert!(removed.all_symbols().is_empty());
}
#[test]
fn duplicate_identities_keep_each_physical_symbol_addressable() {
let sources = vec![
source(0, "java", "module-a/src/test/java/acme/Duplicate.java"),
source(1, "java", "module-b/src/test/java/acme/Duplicate.java"),
];
let identity = "code+moniker://./lang:java/srcset:test/package:acme/class:Duplicate";
let symbols = RecordTable::from_shards(vec![
Arc::from(vec![symbol(0, 0, identity, "Duplicate")]),
Arc::from(vec![symbol(1, 0, identity, "Duplicate")]),
]);
let inventory = SymbolInventoryIndex::build(ResourceGeneration::new(1), &sources, &symbols);
let matches = inventory
.facets()
.symbols_by_identity(identity)
.expect("identity posting");
assert_eq!(matches.len(), 2);
assert_eq!(inventory.catalog().ids(matches).len(), 2);
assert!(inventory.record_by_id(&SymbolId::at(0, 0)).is_some());
assert!(inventory.record_by_id(&SymbolId::at(1, 0)).is_some());
let refreshed_symbols = RecordTable::from_shards(vec![
Arc::from(vec![symbol(0, 1, identity, "Duplicate")]),
Arc::from(vec![symbol(1, 0, identity, "Duplicate")]),
]);
let refreshed = inventory.refresh(
ResourceGeneration::new(2),
&sources,
&refreshed_symbols,
&BTreeSet::from([0]),
);
let matches = refreshed
.facets()
.symbols_by_identity(identity)
.expect("refreshed identity posting");
assert_eq!(matches.len(), 2);
assert!(refreshed.record_by_id(&SymbolId::at(0, 1)).is_some());
assert!(refreshed.record_by_id(&SymbolId::at(1, 0)).is_some());
}
#[test]
fn refresh_churn_does_not_grow_storage_with_retired_ordinal_holes() {
let sources = vec![source(0, "java", "src/main/java/acme/Order.java")];
let initial = RecordTable::from_shards(vec![Arc::from(vec![symbol(
0,
0,
"code+moniker://./lang:java/srcset:main/package:acme/class:Order0",
"Order0",
)])]);
let mut inventory = SymbolInventoryIndex::build(ResourceGeneration::new(1), &sources, &initial);
for generation in 2..=34 {
let name = format!("Order{generation}");
let identity = format!("code+moniker://./lang:java/srcset:main/package:acme/class:{name}");
let symbols = RecordTable::from_shards(vec![Arc::from(vec![symbol(
0, generation, &identity, &name,
)])]);
inventory = inventory.refresh(
ResourceGeneration::new(generation as u64),
&sources,
&symbols,
&BTreeSet::from([0]),
);
}
assert_eq!(inventory.all_symbols().len(), 1);
assert_eq!(inventory.catalog().len(), 1);
assert!(
inventory.catalog().storage_len() <= 2,
"retired ordinals must not leave high-water storage: {} slots for one active symbol",
inventory.catalog().storage_len()
);
assert!(
inventory.records.len() <= 2,
"record storage must stay proportional to active symbols: {} slots",
inventory.records.len()
);
}
#[test]
fn compact_identity_lookup_tracks_build_and_refresh() {
let sources = vec![source(0, "rs", "src/lib.rs")];
let before_identity = "code+moniker://./lang:rs/dir:src/class:Before";
let before = RecordTable::from_shards(vec![Arc::from(vec![symbol(
0,
0,
before_identity,
"before",
)])]);
let inventory = SymbolInventoryIndex::build(ResourceGeneration::new(1), &sources, &before);
let before_compact = crate::code::compact_identity(before_identity, "code+moniker://")
.expect("compact identity");
assert_eq!(
inventory.symbol_ids_by_compact_identity(&before_compact),
vec![SymbolId::at(0, 0)]
);
let after_identity = "code+moniker://./lang:rs/dir:src/class:After";
let after =
RecordTable::from_shards(vec![Arc::from(vec![symbol(0, 0, after_identity, "after")])]);
let refreshed = inventory.refresh(
ResourceGeneration::new(2),
&sources,
&after,
&BTreeSet::from([0]),
);
assert!(
refreshed
.symbol_ids_by_compact_identity(&before_compact)
.is_empty()
);
let after_compact =
crate::code::compact_identity(after_identity, "code+moniker://").expect("compact identity");
assert_eq!(
refreshed.symbol_ids_by_compact_identity(&after_compact),
vec![SymbolId::at(0, 0)]
);
}
#[test]
fn compact_identity_lookup_preserves_and_refreshes_ambiguity() {
let sources = vec![source(0, "rs", "src/lib.rs")];
let primary_identity = "code+moniker://./lang:rs/dir:src/class:Thing";
let alternate_identity = "alternate://project/lang:rs/dir:src/class:Thing";
let ambiguous = RecordTable::from_shards(vec![Arc::from(vec![
symbol(0, 0, primary_identity, "primary"),
symbol(0, 1, alternate_identity, "alternate"),
])]);
let inventory = SymbolInventoryIndex::build(ResourceGeneration::new(1), &sources, &ambiguous);
let compact = crate::code::compact_identity(primary_identity, "code+moniker://")
.expect("compact identity");
assert_eq!(
inventory.symbol_ids_by_compact_identity(&compact),
vec![SymbolId::at(0, 0), SymbolId::at(0, 1)]
);
let unambiguous = RecordTable::from_shards(vec![Arc::from(vec![symbol(
0,
0,
primary_identity,
"primary",
)])]);
let refreshed = inventory.refresh(
ResourceGeneration::new(2),
&sources,
&unambiguous,
&BTreeSet::from([0]),
);
assert_eq!(
refreshed.symbol_ids_by_compact_identity(&compact),
vec![SymbolId::at(0, 0)]
);
}