use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::ffi::{OsStr, OsString};
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, RwLock};
use crate::content::{
AnalysisApplyOutcome, AnalysisCandidate, AnalysisObservation, AnalysisSet, ContentIndex,
ContentRollUp, RestoreCandidate,
};
use crate::engine_contract::{
Attrs, Clock, Commit, Coverage, CoverageReason, DiscoveryProgress, EffectiveChange,
EntryIdentity, EntryKind, Expectation, Freshness, Impact, ImpactDomain, IndexState,
InvalidateReason, Issue, LifecyclePhase, MAX_DIRTY_PATHS, MAX_RETAINED_ISSUES, Observation,
ObservationOp, Op, PathExpectation, PathState, Provenance, ScanScope, Source, StateTransition,
Status, Work,
};
const MAX_VERIFIED_INTERVALS: usize = 256;
fn same_issue_cause(left: &Issue, right: &Issue) -> bool {
left.kind == right.kind && left.path == right.path && (right.path.is_some() || left == right)
}
fn compare_issues(left: &Issue, right: &Issue) -> std::cmp::Ordering {
left.path
.cmp(&right.path)
.then_with(|| issue_kind_rank(left.kind).cmp(&issue_kind_rank(right.kind)))
.then_with(|| left.message.cmp(&right.message))
.then_with(|| left.os_error.cmp(&right.os_error))
}
const fn issue_kind_rank(kind: crate::IssueKind) -> u8 {
match kind {
crate::IssueKind::Permission => 0,
crate::IssueKind::Disappeared => 1,
crate::IssueKind::InvalidMetadata => 2,
crate::IssueKind::ResourceBudget => 3,
crate::IssueKind::ObservationGap => 4,
crate::IssueKind::ProviderFailure => 5,
}
}
#[cfg(test)]
std::thread_local! {
pub(crate) static RECLASSIFY_VISITS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
pub(crate) static CONTROL_PROJECTION_CLONES: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
pub(crate) static ANALYSIS_CHILD_STEPS: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
}
pub const DEFAULT_JOURNAL_CAPACITY_BYTES: usize = 8 * 1024 * 1024;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct EntryId {
slot: u32,
generation: u64,
}
impl EntryId {
pub const ROOT: EntryId = EntryId { slot: 0, generation: 0 };
#[inline]
const fn idx(self) -> usize {
self.slot as usize
}
}
type ExtId = u32;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct ExtTally {
pub files: u64,
pub bytes: u64,
pub allocated: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct RollUp {
pub files: u64,
pub dirs: u64,
pub bytes: u64,
pub allocated: u64,
pub newest_mtime_ns: i64,
pub by_ext: BTreeMap<String, ExtTally>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct PartitionRollUp {
pub all: RollUp,
pub unignored: RollUp,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct RollUpSummary {
pub files: u64,
pub dirs: u64,
pub bytes: u64,
pub allocated: u64,
pub newest_mtime_ns: Option<i64>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct PartitionRollUpSummary {
pub all: RollUpSummary,
pub unignored: RollUpSummary,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
struct InternedRollUp {
files: u64,
dirs: u64,
bytes: u64,
allocated: u64,
newest_mtime_ns: i64,
by_ext: BTreeMap<ExtId, ExtTally>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
struct InternedPartitionRollUp {
all: InternedRollUp,
unignored: InternedRollUp,
}
impl std::ops::Deref for InternedPartitionRollUp {
type Target = InternedRollUp;
fn deref(&self) -> &Self::Target {
&self.all
}
}
impl std::ops::DerefMut for InternedPartitionRollUp {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.all
}
}
impl InternedPartitionRollUp {
fn merge(&mut self, other: &Self) {
self.all.merge(&other.all);
self.unignored.merge(&other.unignored);
}
fn unmerge(&mut self, other: &Self) {
self.all.unmerge(&other.all);
self.unignored.unmerge(&other.unignored);
}
}
fn rollup_summary(rollup: &InternedRollUp) -> RollUpSummary {
RollUpSummary {
files: rollup.files,
dirs: rollup.dirs,
bytes: rollup.bytes,
allocated: rollup.allocated,
newest_mtime_ns: (rollup.files > 0).then_some(rollup.newest_mtime_ns),
}
}
fn partition_summary(rollup: &InternedPartitionRollUp) -> PartitionRollUpSummary {
PartitionRollUpSummary {
all: rollup_summary(&rollup.all),
unignored: rollup_summary(&rollup.unignored),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(crate) struct RollUpScalars {
pub(crate) files: u64,
pub(crate) dirs: u64,
pub(crate) bytes: u64,
pub(crate) allocated: u64,
pub(crate) newest_mtime_ns: i64,
}
impl From<&InternedRollUp> for RollUpScalars {
fn from(rollup: &InternedRollUp) -> Self {
Self {
files: rollup.files,
dirs: rollup.dirs,
bytes: rollup.bytes,
allocated: rollup.allocated,
newest_mtime_ns: rollup.newest_mtime_ns,
}
}
}
impl RollUpScalars {
fn leaf(kind: EntryKind, attrs: &Attrs) -> Self {
match kind {
EntryKind::File => {
Self { files: 1, bytes: attrs.size, allocated: attrs.allocated, ..Self::default() }
}
EntryKind::Dir => Self { dirs: 1, ..Self::default() },
EntryKind::Symlink | EntryKind::Other => Self::default(),
}
}
fn checked_add(self, other: &Self) -> Result<Self, &'static str> {
Ok(Self {
files: self.files.checked_add(other.files).ok_or("files")?,
dirs: self.dirs.checked_add(other.dirs).ok_or("directories")?,
bytes: self.bytes.checked_add(other.bytes).ok_or("bytes")?,
allocated: self.allocated.checked_add(other.allocated).ok_or("allocated bytes")?,
newest_mtime_ns: self.newest_mtime_ns,
})
}
fn saturating_sub(self, other: &Self) -> Self {
Self {
files: self.files.saturating_sub(other.files),
dirs: self.dirs.saturating_sub(other.dirs),
bytes: self.bytes.saturating_sub(other.bytes),
allocated: self.allocated.saturating_sub(other.allocated),
newest_mtime_ns: self.newest_mtime_ns,
}
}
}
#[inline]
pub(crate) fn add_file_sizes(
bytes: &mut u64,
allocated: &mut u64,
attrs: &Attrs,
path: impl FnOnce() -> PathBuf,
) -> crate::Result<()> {
match (bytes.checked_add(attrs.size), allocated.checked_add(attrs.allocated)) {
(Some(next_bytes), Some(next_allocated)) => {
*bytes = next_bytes;
*allocated = next_allocated;
Ok(())
}
(None, _) => Err(unrepresentable_file(path(), "bytes")),
(Some(_), None) => Err(unrepresentable_file(path(), "allocated bytes")),
}
}
#[cold]
#[inline(never)]
fn unrepresentable_file(path: PathBuf, counter: &'static str) -> crate::Error {
crate::Error::UnrepresentableTotal { path, counter }
}
#[derive(Default)]
struct TotalsOverlay {
cuts: BTreeMap<PathBuf, RollUpScalars>,
live: BTreeMap<PathBuf, (EntryKind, RollUpScalars)>,
}
impl TotalsOverlay {
fn index_alive(&self, index: &Index, path: &Path) -> Option<EntryId> {
let id = index.lookup(path)?;
let mut ancestor = Some(path);
while let Some(current) = ancestor {
if self.cuts.contains_key(current) {
return None;
}
ancestor = current.parent();
}
Some(id)
}
fn kind_at(&self, index: &Index, path: &Path) -> Option<EntryKind> {
if let Some((kind, _)) = self.live.get(path) {
return Some(*kind);
}
self.index_alive(index, path).map(|id| index.entry(id).kind)
}
fn beneath<'a, V>(
map: &'a BTreeMap<PathBuf, V>,
path: &'a Path,
) -> impl Iterator<Item = (&'a PathBuf, &'a V)> {
map.range::<Path, _>((std::ops::Bound::Excluded(path), std::ops::Bound::Unbounded))
.take_while(move |(candidate, _)| candidate.starts_with(path))
}
fn subtree_at(&self, index: &Index, path: &Path) -> RollUpScalars {
let mut total = RollUpScalars::default();
if let Some(id) = self.index_alive(index, path) {
total = index.subtree_scalars(id);
for (_, cut) in Self::beneath(&self.cuts, path) {
total = total.saturating_sub(cut);
}
}
let live = self
.live
.range::<Path, _>((std::ops::Bound::Included(path), std::ops::Bound::Unbounded))
.take_while(|(candidate, _)| candidate.starts_with(path));
for (_, (_, own)) in live {
total = total.checked_add(own).unwrap_or_else(|_| {
unreachable!("a subtree the tree holds is within its representable total")
});
}
total
}
fn cut(&mut self, index: &Index, path: &Path) {
if let Some(id) = self.index_alive(index, path) {
let covered: Vec<PathBuf> =
Self::beneath(&self.cuts, path).map(|(cut, _)| cut.clone()).collect();
for cut in covered {
self.cuts.remove(&cut);
}
self.cuts.insert(path.to_path_buf(), index.subtree_scalars(id));
}
let gone: Vec<PathBuf> = self
.live
.range::<Path, _>((std::ops::Bound::Included(path), std::ops::Bound::Unbounded))
.take_while(|(candidate, _)| candidate.starts_with(path))
.map(|(live, _)| live.clone())
.collect();
for live in gone {
self.live.remove(&live);
}
}
fn replace(&mut self, index: &Index, path: &Path, kind: EntryKind, own: RollUpScalars) {
self.cut(index, path);
self.live.insert(path.to_path_buf(), (kind, own));
}
}
#[derive(Debug)]
pub(crate) struct AnalysisWalk {
pending: Vec<(EntryId, PathBuf)>,
listing: Option<(EntryId, PathBuf, ListingResume)>,
}
impl AnalysisWalk {
pub(crate) fn start() -> Self {
Self { pending: vec![(EntryId::ROOT, PathBuf::new())], listing: None }
}
}
#[derive(Debug)]
struct ListingResume {
position: usize,
last: OsString,
}
enum ResumedChildren<'a> {
Empty,
Sorted { index: &'a Index, ids: std::slice::Iter<'a, EntryId> },
Mutable(std::collections::btree_map::Range<'a, OsString, EntryId>),
}
impl<'a> Iterator for ResumedChildren<'a> {
type Item = (&'a OsStr, EntryId);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::Sorted { index, ids } => {
let id = *ids.next()?;
Some((index.entry(id).name.as_os_str(), id))
}
Self::Mutable(children) => children.next().map(|(name, id)| (name.as_os_str(), *id)),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct FoldedFiles {
pub(crate) files: u64,
pub(crate) bytes: u64,
pub(crate) allocated: u64,
pub(crate) ignored: crate::query::IgnoredSize,
}
impl FoldedFiles {
fn add(&mut self, attrs: &Attrs, ignored: bool) {
self.files += 1;
self.bytes += attrs.size;
self.allocated += attrs.allocated;
if ignored {
self.ignored.bytes += attrs.size;
self.ignored.allocated += attrs.allocated;
}
}
}
impl InternedRollUp {
fn merge(&mut self, other: &InternedRollUp) {
let had_files = self.files > 0;
self.files += other.files;
self.dirs += other.dirs;
self.bytes += other.bytes;
self.allocated += other.allocated;
if other.files > 0 {
self.newest_mtime_ns = if had_files {
self.newest_mtime_ns.max(other.newest_mtime_ns)
} else {
other.newest_mtime_ns
};
}
for (ext, tally) in &other.by_ext {
let slot = self.by_ext.entry(*ext).or_default();
slot.files += tally.files;
slot.bytes += tally.bytes;
slot.allocated += tally.allocated;
}
}
fn unmerge(&mut self, other: &InternedRollUp) {
self.files = self.files.saturating_sub(other.files);
self.dirs = self.dirs.saturating_sub(other.dirs);
self.bytes = self.bytes.saturating_sub(other.bytes);
self.allocated = self.allocated.saturating_sub(other.allocated);
for (ext, tally) in &other.by_ext {
if let Some(slot) = self.by_ext.get_mut(ext) {
slot.files = slot.files.saturating_sub(tally.files);
slot.bytes = slot.bytes.saturating_sub(tally.bytes);
slot.allocated = slot.allocated.saturating_sub(tally.allocated);
if slot.files == 0 && slot.bytes == 0 && slot.allocated == 0 {
self.by_ext.remove(ext);
}
}
}
}
}
#[derive(Clone, Debug)]
enum DirectoryChildren {
Sorted(Vec<EntryId>),
Mutable(BTreeMap<OsString, EntryId>),
}
impl DirectoryChildren {
#[cfg(test)]
fn is_sorted(&self) -> bool {
matches!(self, Self::Sorted(_))
}
#[cfg(test)]
fn is_mutable(&self) -> bool {
matches!(self, Self::Mutable(_))
}
fn ids(&self) -> ChildIds<'_> {
match self {
Self::Sorted(ids) => ChildIds::Sorted(ids.iter()),
Self::Mutable(children) => ChildIds::Mutable(children.values()),
}
}
}
#[derive(Clone, Debug)]
struct DirectoryEntry {
children: DirectoryChildren,
rollup: InternedPartitionRollUp,
children_revision: u64,
children_complete: bool,
}
impl DirectoryEntry {
fn new(children_complete: bool) -> Self {
Self {
children: DirectoryChildren::Mutable(BTreeMap::new()),
rollup: InternedPartitionRollUp::default(),
children_revision: 0,
children_complete,
}
}
}
#[derive(Clone, Debug)]
struct Entry {
parent: Option<EntryId>,
name: OsString,
ext_id: Option<ExtId>,
ignored: bool,
source: Source,
kind: EntryKind,
attrs: Attrs,
revision: u64,
directory: Option<Box<DirectoryEntry>>,
}
struct NewEntry {
parent: Option<EntryId>,
name: OsString,
ext_id: Option<ExtId>,
ignored: bool,
source: Source,
kind: EntryKind,
attrs: Attrs,
}
impl Entry {
fn new(entry: NewEntry, children_complete: bool) -> Self {
let NewEntry { parent, name, ext_id, ignored, source, kind, attrs } = entry;
Self {
parent,
name,
ext_id,
ignored,
source,
kind,
attrs,
revision: 0,
directory: kind.is_dir().then(|| Box::new(DirectoryEntry::new(children_complete))),
}
}
fn new_detached(new_entry: NewEntry, children_complete: bool) -> Self {
let mut entry = Self::new(new_entry, children_complete);
if let Some(directory) = entry.directory.as_deref_mut() {
directory.children = DirectoryChildren::Sorted(Vec::new());
}
entry
}
fn directory(&self) -> &DirectoryEntry {
self.directory.as_deref().expect("directory entry must retain directory state")
}
fn directory_mut(&mut self) -> &mut DirectoryEntry {
self.directory.as_deref_mut().expect("directory entry must retain directory state")
}
fn rollup(&self) -> &InternedPartitionRollUp {
&self.directory().rollup
}
fn rollup_mut(&mut self) -> &mut InternedPartitionRollUp {
&mut self.directory_mut().rollup
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub(crate) struct PortableChildren {
pub(crate) directories: BTreeMap<String, EntryId>,
pub(crate) nondirectories: BTreeMap<String, EntryId>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
struct ServingIndexes {
portable_children: BTreeMap<PathBuf, PortableChildren>,
portable_entries: BTreeMap<crate::PortablePath, EntryId>,
recent_files: BTreeSet<RecentKey>,
semantic_names: Vec<Option<String>>,
semantic_ids: BTreeMap<String, u32>,
semantic_refcounts: Vec<u64>,
free_semantic_ids: Vec<u32>,
semantic_by_directory: BTreeMap<EntryId, InternedSemanticPartitions>,
exact_name_ids: BTreeMap<String, u32>,
exact_names: Vec<String>,
exact_name_by_directory: BTreeMap<EntryId, InternedSemanticPartitions>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
struct InternedSemanticPartitions {
all: BTreeMap<u32, ExtTally>,
unignored: BTreeMap<u32, ExtTally>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct RecentKey {
mtime_ns: i64,
portable_path: crate::PortablePath,
id: EntryId,
}
impl PartialOrd for RecentKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RecentKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other
.mtime_ns
.cmp(&self.mtime_ns)
.then_with(|| self.portable_path.cmp(&other.portable_path))
.then_with(|| self.id.cmp(&other.id))
}
}
impl ServingIndexes {
fn for_types(types: &crate::classify::TypeRegistry) -> Self {
let exact_names: Vec<_> = types
.exact_filenames()
.map(str::to_ascii_lowercase)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let exact_name_ids = exact_names
.iter()
.enumerate()
.map(|(index, name)| {
let id = u32::try_from(index)
.expect("a registry declares fewer than four billion exact filenames");
(name.clone(), id)
})
.collect();
Self { exact_name_ids, exact_names, ..Self::default() }
}
fn exact_name_id(&self, name: &OsStr) -> Option<u32> {
let name = name.to_str()?;
if let Some(id) = self.exact_name_ids.get(name) {
return Some(*id);
}
name.bytes()
.any(|byte| byte.is_ascii_uppercase())
.then(|| name.to_ascii_lowercase())
.and_then(|name| self.exact_name_ids.get(&name).copied())
}
fn intern_semantic(&mut self, name: &str) -> u32 {
if let Some(id) = self.semantic_ids.get(name).copied() {
let refcount = self
.semantic_refcounts
.get_mut(id as usize)
.expect("a live semantic id has a refcount");
*refcount = refcount.checked_add(1).expect("semantic refcount exhausted");
return id;
}
let id = if let Some(id) = self.free_semantic_ids.pop() {
self.semantic_names[id as usize] = Some(name.to_string());
self.semantic_refcounts[id as usize] = 1;
id
} else {
let id = u32::try_from(self.semantic_names.len())
.expect("fewer than four billion semantic types are live");
self.semantic_names.push(Some(name.to_string()));
self.semantic_refcounts.push(1);
id
};
self.semantic_ids.insert(name.to_string(), id);
id
}
fn release_semantic(&mut self, id: u32, count: u64) {
let slot = self
.semantic_refcounts
.get_mut(id as usize)
.expect("a live semantic id has a refcount");
*slot = slot.checked_sub(count).expect("semantic reference released twice");
if *slot != 0 {
return;
}
let name =
self.semantic_names[id as usize].take().expect("a referenced semantic id has a name");
let removed = self.semantic_ids.remove(&name);
debug_assert_eq!(removed, Some(id), "the semantic interner's two maps disagreed");
self.free_semantic_ids.push(id);
}
}
fn merge_semantic(map: &mut BTreeMap<u32, ExtTally>, id: u32, attrs: Attrs) {
let tally = map.entry(id).or_default();
tally.files = tally.files.saturating_add(1);
tally.bytes = tally.bytes.saturating_add(attrs.size);
tally.allocated = tally.allocated.saturating_add(attrs.allocated);
}
fn unmerge_semantic(map: &mut BTreeMap<u32, ExtTally>, id: u32, attrs: Attrs) {
let tally = map.get_mut(&id).expect("a semantic contribution must exist before removal");
tally.files = tally.files.saturating_sub(1);
tally.bytes = tally.bytes.saturating_sub(attrs.size);
tally.allocated = tally.allocated.saturating_sub(attrs.allocated);
if tally.files == 0 && tally.bytes == 0 && tally.allocated == 0 {
map.remove(&id);
}
}
fn unmerge_semantic_map(
destination: &mut BTreeMap<u32, ExtTally>,
contribution: &BTreeMap<u32, ExtTally>,
) {
for (id, removed) in contribution {
let tally = destination
.get_mut(id)
.expect("a semantic subtree contribution must exist before removal");
tally.files = tally.files.saturating_sub(removed.files);
tally.bytes = tally.bytes.saturating_sub(removed.bytes);
tally.allocated = tally.allocated.saturating_sub(removed.allocated);
if tally.files == 0 && tally.bytes == 0 && tally.allocated == 0 {
destination.remove(id);
}
}
}
#[derive(Clone, Debug)]
enum Slot {
Occupied { generation: u64, entry: Entry },
Free { generation: u64, next_free: Option<u32> },
}
fn retained_parent(arena: &[Slot], id: EntryId) -> Option<EntryId> {
match arena.get(id.idx()) {
Some(Slot::Occupied { generation, entry }) if *generation == id.generation => entry.parent,
Some(Slot::Occupied { .. } | Slot::Free { .. }) | None => {
panic!("internal entry handle must be live: {id:?}")
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[must_use]
pub struct Since {
pub commits: Vec<Commit>,
pub clock: Clock,
pub state: IndexState,
pub truncated: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct ApplyStats {
pub inserted: u64,
pub updated: u64,
pub removed: u64,
pub unchanged: u64,
pub invalidated: u64,
pub controls: u64,
pub reclassified: u64,
pub stale: u64,
pub resource_refused: u64,
}
impl ApplyStats {
pub const fn mutated(&self) -> bool {
self.inserted > 0
|| self.updated > 0
|| self.removed > 0
|| self.invalidated > 0
|| self.controls > 0
|| self.reclassified > 0
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct ApplyOutcome {
pub stats: ApplyStats,
pub commit: Option<Commit>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ChildSnapshot {
pub id: EntryId,
pub name: OsString,
pub kind: EntryKind,
pub attrs: Attrs,
pub ignored: Option<bool>,
pub rollup: Option<RollUp>,
pub partitions: Option<PartitionRollUp>,
}
impl std::ops::Deref for ApplyOutcome {
type Target = ApplyStats;
fn deref(&self) -> &Self::Target {
&self.stats
}
}
impl ApplyOutcome {
fn from_commit(stats: ApplyStats, commit: Option<Commit>) -> Self {
Self { stats, commit }
}
}
#[derive(Clone, Copy)]
enum BatchProvenance {
Baseline,
Opened,
Public,
}
fn record_batch(provenance: BatchProvenance, observed: usize, stats: ApplyStats) {
let observed = u64::try_from(observed).unwrap_or(u64::MAX);
let accepted = observed.saturating_sub(stats.stale);
crate::counters::bump(|counts| match provenance {
BatchProvenance::Baseline => {
counts.baseline_batches = counts.baseline_batches.saturating_add(1);
counts.baseline_accepted_ops = counts.baseline_accepted_ops.saturating_add(accepted);
}
BatchProvenance::Opened => {
counts.opened_batches = counts.opened_batches.saturating_add(1);
counts.opened_accepted_ops = counts.opened_accepted_ops.saturating_add(accepted);
}
BatchProvenance::Public => {
counts.public_batches = counts.public_batches.saturating_add(1);
counts.public_accepted_ops = counts.public_accepted_ops.saturating_add(accepted);
}
});
}
fn elapsed_micros(started: std::time::Instant) -> u64 {
u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX)
}
#[derive(Clone, Debug)]
struct PreparedObservation {
ops: Vec<ObservationOp>,
ancestry: PreparedAncestry,
#[cfg(test)]
reject_before_apply: bool,
}
#[derive(Clone, Debug)]
enum PreparedAncestry {
General,
Scanner { parents: Vec<ResolvedParent>, has_batch_parents: bool },
}
#[derive(Clone, Copy, Debug)]
enum ResolvedParent {
Existing(EntryId),
Earlier(usize),
}
#[derive(Default)]
struct ExactConsequences {
changes: Vec<EffectiveChange>,
state: Vec<StateTransition>,
}
impl ExactConsequences {
fn is_empty(&self) -> bool {
self.changes.is_empty() && self.state.is_empty()
}
}
#[derive(Default)]
struct NoConsequences;
trait ConsequenceSink {
fn change(&mut self, change: impl FnOnce() -> EffectiveChange);
fn state(&mut self, transition: impl FnOnce() -> StateTransition);
}
impl ConsequenceSink for ExactConsequences {
#[inline]
fn change(&mut self, change: impl FnOnce() -> EffectiveChange) {
self.changes.push(change());
}
#[inline]
fn state(&mut self, transition: impl FnOnce() -> StateTransition) {
self.state.push(transition());
}
}
impl ConsequenceSink for NoConsequences {
#[inline]
fn change(&mut self, _change: impl FnOnce() -> EffectiveChange) {}
#[inline]
fn state(&mut self, _transition: impl FnOnce() -> StateTransition) {}
}
#[derive(Clone, Debug)]
pub struct Index {
root_path: PathBuf,
scope: ScanScope,
arena: Vec<Slot>,
free_head: Option<u32>,
live: u64,
clock: Clock,
journal: VecDeque<Commit>,
journal_cost: usize,
journal_capacity_bytes: usize,
journal_floor: Clock,
pending_invalidations: Vec<(PathBuf, InvalidateReason)>,
applying_source: Source,
scanned_at_ns: i64,
captured_at_ns: i64,
writing_pass_started_at_ns: i64,
persistence_owed: bool,
active_root_reconciles: BTreeMap<u64, i64>,
active_reconciles: BTreeMap<u64, ActiveReconcile>,
verified: Vec<(PathBuf, i64)>,
ext_names: Vec<Option<String>>,
ext_ids: BTreeMap<String, ExtId>,
ext_refcounts: Vec<u64>,
free_ext_ids: Vec<ExtId>,
content: Option<Box<ContentIndex>>,
types: std::sync::Arc<crate::classify::TypeRegistry>,
controls: crate::control::ControlTable,
unreadable_control_paths: BTreeSet<PathBuf>,
freshness_epoch: u64,
freshness_marks: BTreeMap<PathBuf, FreshnessMark>,
state: IndexState,
issues: Vec<Issue>,
issue_epochs: Vec<u64>,
omitted_issue_epochs: BTreeMap<u64, u64>,
serving: Option<Box<ServingIndexes>>,
folded: Option<Vec<FoldedFiles>>,
}
#[derive(Clone, Copy)]
pub(crate) struct ReconcileErrors<'a> {
pub(crate) errors: &'a [crate::Error],
pub(crate) terminal: Option<&'a crate::Error>,
pub(crate) disproves_old: bool,
}
#[derive(Clone, Debug)]
struct ActiveReconcile {
path: PathBuf,
scope_budget: usize,
evidence: ReconcileEvidence,
}
#[derive(Clone, Debug)]
enum ReconcileEvidence {
Scopes(BTreeSet<PathBuf>),
Retry,
}
impl ActiveReconcile {
fn supersede(&mut self, path: &Path) {
if self.path.starts_with(path) {
self.evidence = ReconcileEvidence::Scopes(BTreeSet::from([path.to_path_buf()]));
return;
}
let ReconcileEvidence::Scopes(scopes) = &mut self.evidence else {
return;
};
if scopes.iter().any(|newer| path.starts_with(newer)) {
return;
}
scopes.retain(|newer| !newer.starts_with(path));
if scopes.len() == self.scope_budget {
self.evidence = ReconcileEvidence::Retry;
} else {
scopes.insert(path.to_path_buf());
}
}
fn refuses(&self, observation: &Observation, index: &Index) -> bool {
match &self.evidence {
ReconcileEvidence::Retry => true,
ReconcileEvidence::Scopes(scopes) => observation.ops.iter().any(|op| {
scopes
.iter()
.any(|path| op.op.path().starts_with(path) || path.starts_with(op.op.path()))
&& !index.holds_target(&op.op, index.path_state(op.op.path()))
}),
}
}
}
pub(crate) struct ReconcileFinish {
pub(crate) commit: Option<Commit>,
pub(crate) retry: bool,
}
enum ChildIds<'a> {
Sorted(std::slice::Iter<'a, EntryId>),
Mutable(std::collections::btree_map::Values<'a, OsString, EntryId>),
}
impl Iterator for ChildIds<'_> {
type Item = EntryId;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Sorted(ids) => ids.next().copied(),
Self::Mutable(ids) => ids.next().copied(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
}
impl DoubleEndedIterator for ChildIds<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
match self {
Self::Sorted(ids) => ids.next_back().copied(),
Self::Mutable(ids) => ids.next_back().copied(),
}
}
}
impl ExactSizeIterator for ChildIds<'_> {
fn len(&self) -> usize {
match self {
Self::Sorted(ids) => ids.len(),
Self::Mutable(ids) => ids.len(),
}
}
}
enum IndexChildren<'a> {
Empty,
Sorted { index: &'a Index, ids: std::slice::Iter<'a, EntryId> },
Mutable(std::collections::btree_map::Iter<'a, OsString, EntryId>),
}
impl<'a> IndexChildren<'a> {
fn new(index: &'a Index, entry: &'a Entry) -> Self {
match entry.directory.as_deref().map(|directory| &directory.children) {
None => Self::Empty,
Some(DirectoryChildren::Sorted(ids)) => Self::Sorted { index, ids: ids.iter() },
Some(DirectoryChildren::Mutable(children)) => Self::Mutable(children.iter()),
}
}
}
impl<'a> Iterator for IndexChildren<'a> {
type Item = (&'a OsStr, EntryId);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::Sorted { index, ids } => {
let id = *ids.next()?;
Some((index.entry(id).name.as_os_str(), id))
}
Self::Mutable(children) => children.next().map(|(name, id)| (name.as_os_str(), *id)),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
}
impl DoubleEndedIterator for IndexChildren<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::Sorted { index, ids } => {
let id = *ids.next_back()?;
Some((index.entry(id).name.as_os_str(), id))
}
Self::Mutable(children) => {
children.next_back().map(|(name, id)| (name.as_os_str(), *id))
}
}
}
}
impl ExactSizeIterator for IndexChildren<'_> {
fn len(&self) -> usize {
match self {
Self::Empty => 0,
Self::Sorted { ids, .. } => ids.len(),
Self::Mutable(children) => children.len(),
}
}
}
#[derive(Clone, Copy, Debug)]
struct FreshnessMark {
state: Freshness,
epoch: u64,
}
#[derive(Clone, Debug)]
pub struct IndexHandle {
inner: Arc<RwLock<Index>>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct DiscoveryCommit {
pub(crate) directory_complete: Option<PathBuf>,
pub(crate) transition: Option<DiscoveryTransition>,
}
#[derive(Clone, Debug)]
pub(crate) enum DiscoveryTransition {
Begin,
Finish,
BudgetRefused(Issue),
Inaccessible { issues: Vec<Issue>, omitted: u64 },
Cancelled,
Failed(Issue),
}
#[derive(Clone, Debug)]
#[cfg_attr(not(feature = "watch"), allow(dead_code))]
pub(crate) enum ObservationTransition {
Reconciling,
Watching { issues: Vec<Issue>, omitted: u64 },
Unreadable { issues: Vec<Issue>, omitted: u64 },
Failed(Issue),
}
impl IndexHandle {
pub fn new(index: Index) -> Self {
Self { inner: Arc::new(RwLock::new(index)) }
}
fn read_index(&self) -> crate::Result<std::sync::RwLockReadGuard<'_, Index>> {
self.inner.read().map_err(|_| crate::Error::IndexLockPoisoned)
}
fn write_index(&self) -> crate::Result<std::sync::RwLockWriteGuard<'_, Index>> {
self.inner.write().map_err(|_| crate::Error::IndexLockPoisoned)
}
pub(crate) fn read_with<T>(&self, read: impl FnOnce(&Index) -> T) -> crate::Result<T> {
let index = self.read_index()?;
Ok(read(&index))
}
#[cfg(test)]
pub(crate) fn poison_for_test(&self) {
let handle = self.clone();
std::thread::spawn(move || handle.panic_holding_the_write_lock_for_test())
.join()
.expect_err("injected index panic");
}
#[cfg(test)]
pub(crate) fn panic_holding_the_write_lock_for_test(&self) -> ! {
let _guard = self.inner.write().expect("test index write lock");
panic!("inject index poison");
}
pub fn apply(&self, observation: &Observation) -> crate::Result<ApplyOutcome> {
let prepared = prepare_observation(observation)?;
let outcome = self.write_index()?.commit_prepared(prepared, true)?;
record_batch(BatchProvenance::Public, observation.len(), outcome.stats);
Ok(outcome)
}
pub(crate) fn apply_discovery(
&self,
observation: &Observation,
discovery: DiscoveryCommit,
) -> crate::Result<ApplyOutcome> {
let prepared = prepare_observation(observation)?;
let outcome = self.write_index()?.commit_prepared_with(
prepared,
true,
Some(discovery),
None,
None,
true,
)?;
record_batch(BatchProvenance::Opened, observation.len(), outcome.stats);
Ok(outcome)
}
#[cfg(feature = "watch")]
pub(crate) fn apply_opened(
&self,
observation: &Observation,
max_files: Option<u64>,
) -> crate::Result<ApplyOutcome> {
let prepared = prepare_observation(observation)?;
let outcome = self
.write_index()?
.commit_prepared_with(prepared, true, None, None, max_files, true)?;
record_batch(BatchProvenance::Opened, observation.len(), outcome.stats);
Ok(outcome)
}
pub(crate) fn apply_scanner_discovery_bounded(
&self,
batch: crate::scan::ScannerBatch,
discovery: DiscoveryCommit,
max_files: Option<u64>,
) -> crate::Result<ApplyOutcome> {
let observed = batch.len();
let mut index = self.write_index()?;
let prepared = index.prepare_scanner_batch(batch)?;
let outcome =
index.commit_prepared_with(prepared, true, Some(discovery), None, max_files, true)?;
record_batch(BatchProvenance::Opened, observed, outcome.stats);
Ok(outcome)
}
pub(crate) fn transition_discovery(
&self,
transition: DiscoveryTransition,
) -> crate::Result<ApplyOutcome> {
self.apply_discovery(
&Observation::new(Vec::new()),
DiscoveryCommit { directory_complete: None, transition: Some(transition) },
)
}
#[cfg(feature = "watch")]
pub(crate) fn transition_observation(
&self,
transition: ObservationTransition,
) -> crate::Result<ApplyOutcome> {
let prepared = prepare_observation(&Observation::default())?;
let outcome = self.write_index()?.commit_prepared_with(
prepared,
true,
None,
Some(transition),
None,
true,
)?;
record_batch(BatchProvenance::Opened, 0, outcome.stats);
Ok(outcome)
}
pub fn root_path(&self) -> crate::Result<PathBuf> {
Ok(self.read_index()?.root_path().to_path_buf())
}
pub fn scope(&self) -> crate::Result<ScanScope> {
Ok(self.read_index()?.scope())
}
pub fn freshness(&self) -> crate::Result<Freshness> {
Ok(self.read_index()?.freshness())
}
pub fn freshness_at(&self, path: &Path) -> crate::Result<Freshness> {
Ok(self.read_index()?.freshness_at(path))
}
pub fn clock(&self) -> crate::Result<Clock> {
Ok(self.read_index()?.clock())
}
pub fn len(&self) -> crate::Result<u64> {
Ok(self.read_index()?.len())
}
pub fn is_empty(&self) -> crate::Result<bool> {
Ok(self.read_index()?.is_empty())
}
pub fn total(&self) -> crate::Result<RollUp> {
Ok(self.read_index()?.total())
}
pub(crate) fn state(&self) -> crate::Result<IndexState> {
Ok(self.read_index()?.state())
}
#[allow(dead_code)] pub(crate) fn issues(&self) -> crate::Result<Vec<Issue>> {
Ok(self.read_index()?.issues().to_vec())
}
#[allow(dead_code)] pub(crate) fn directory_complete(&self, path: &Path) -> crate::Result<Option<bool>> {
Ok(self.read_index()?.directory_complete(path))
}
pub fn rollup(&self, path: &Path) -> crate::Result<Option<RollUp>> {
Ok(self.read_index()?.rollup(path))
}
pub fn attrs(&self, path: &Path) -> crate::Result<Option<Attrs>> {
Ok(self.read_index()?.attrs(path).copied())
}
pub fn kind(&self, path: &Path) -> crate::Result<Option<EntryKind>> {
Ok(self.read_index()?.kind(path))
}
pub fn path_state(&self, path: &Path) -> crate::Result<PathState> {
Ok(self.read_index()?.path_state(path))
}
pub fn expectation(&self, path: &Path) -> crate::Result<PathExpectation> {
Ok(self.read_index()?.expectation(path))
}
pub fn since(&self, clock: Clock) -> crate::Result<Since> {
Ok(self.read_index()?.since(clock))
}
pub fn children(&self, path: &Path) -> crate::Result<Option<Vec<ChildSnapshot>>> {
let index = self.read_index()?;
let Some(children) = index.children(path) else {
return Ok(None);
};
let observed = index.observes_controls();
Ok(Some(
children
.map(|(name, id)| {
let entry = index.entry(id);
ChildSnapshot {
id,
name: name.to_os_string(),
kind: entry.kind,
attrs: entry.attrs,
ignored: observed.then_some(entry.ignored),
rollup: entry
.kind
.is_dir()
.then(|| index.named_rollup(&entry.rollup().all)),
partitions: (observed && entry.kind.is_dir())
.then(|| index.named_partitions(entry.rollup())),
}
})
.collect(),
))
}
pub fn snapshot(&self) -> crate::Result<Index> {
let mut snapshot = self.read_index()?.clone();
snapshot.serving = None;
Ok(snapshot)
}
pub(crate) fn child_states(
&self,
path: &Path,
) -> crate::Result<BTreeMap<OsString, PathExpectation>> {
let index = self.read_index()?;
Ok(collect_child_expectations(&index, path))
}
pub(crate) fn listing_baseline(
&self,
path: &Path,
) -> crate::Result<(BTreeMap<OsString, PathExpectation>, bool)> {
let index = self.read_index()?;
Ok((collect_child_expectations(&index, path), index.directory_complete(path) != Some(true)))
}
pub(crate) fn has_control(&self, path: &Path) -> crate::Result<bool> {
Ok(self.read_index()?.control_table().contains(path))
}
pub(crate) fn take_pending_invalidations(
&self,
) -> crate::Result<Vec<(PathBuf, InvalidateReason)>> {
Ok(self.write_index()?.take_pending_invalidations())
}
pub(crate) fn restore_pending_invalidations(
&self,
invalidations: Vec<(PathBuf, InvalidateReason)>,
) -> crate::Result<()> {
self.write_index()?.restore_pending_invalidations(invalidations);
Ok(())
}
pub(crate) fn begin_reconcile(&self, path: &Path) -> crate::Result<(u64, Option<Commit>)> {
self.write_index()?.begin_reconcile(path)
}
pub(crate) fn finish_reconcile(
&self,
path: &Path,
started_at: u64,
complete: bool,
listed_incomplete: &[PathBuf],
failed_paths: &[PathBuf],
errors: ReconcileErrors<'_>,
) -> crate::Result<ReconcileFinish> {
self.write_index()?.finish_reconcile(
path,
started_at,
complete,
listed_incomplete,
failed_paths,
errors,
)
}
pub(crate) fn apply_reconcile(
&self,
started_at: u64,
observation: &Observation,
) -> crate::Result<ApplyOutcome> {
self.apply_reconcile_with(started_at, observation, None, BatchProvenance::Public)
}
pub(crate) fn apply_opened_reconcile(
&self,
started_at: u64,
observation: &Observation,
max_files: Option<u64>,
) -> crate::Result<ApplyOutcome> {
self.apply_reconcile_with(started_at, observation, max_files, BatchProvenance::Opened)
}
fn apply_reconcile_with(
&self,
started_at: u64,
observation: &Observation,
max_files: Option<u64>,
provenance: BatchProvenance,
) -> crate::Result<ApplyOutcome> {
let prepared = prepare_observation(observation)?;
let mut index = self.write_index()?;
if index
.active_reconciles
.get(&started_at)
.is_some_and(|active| active.refuses(observation, &index))
{
let stats = ApplyStats {
stale: u64::try_from(observation.len()).unwrap_or(u64::MAX),
..ApplyStats::default()
};
record_batch(provenance, observation.len(), stats);
return Ok(ApplyOutcome { stats, commit: None });
}
let outcome = index.commit_prepared_with(
prepared,
true,
None,
None,
max_files,
matches!(provenance, BatchProvenance::Opened),
)?;
record_batch(provenance, observation.len(), outcome.stats);
Ok(outcome)
}
#[cfg(feature = "watch")]
pub(crate) fn apply_if_clock(
&self,
clock: Clock,
observation: &Observation,
) -> crate::Result<Option<ApplyOutcome>> {
let prepared = prepare_observation(observation)?;
let mut index = self.write_index()?;
if index.clock() != clock {
return Ok(None);
}
index.commit_prepared(prepared, true).map(Some)
}
#[cfg(feature = "watch")]
pub(crate) fn apply_opened_if_clock(
&self,
clock: Clock,
observation: &Observation,
max_files: Option<u64>,
) -> crate::Result<Option<ApplyOutcome>> {
let prepared = prepare_observation(observation)?;
let mut index = self.write_index()?;
if index.clock() != clock {
return Ok(None);
}
index.commit_prepared_with(prepared, true, None, None, max_files, true).map(Some)
}
#[cfg(feature = "watch")]
pub(crate) fn unknown_ancestry(
&self,
observation: &Observation,
) -> crate::Result<Vec<(PathBuf, PathBuf)>> {
let prepared = prepare_observation(observation)?;
let index = self.read_index()?;
let accepted = index.accepted_operations(&prepared.ops);
Ok(index.unknown_ancestry(&prepared.ops, &accepted))
}
#[cfg(feature = "watch")]
pub(crate) fn watch_boundary(&self) -> crate::Result<(PathBuf, ScanScope, Clock)> {
let index = self.read_index()?;
Ok((index.root_path().to_path_buf(), index.scope(), index.clock()))
}
#[cfg(feature = "watch")]
pub(crate) fn invalidate_root(&self, reason: InvalidateReason) -> crate::Result<ApplyOutcome> {
self.apply(&Observation::new(vec![Op::InvalidateSubtree { path: PathBuf::new(), reason }]))
}
}
pub(crate) struct DetachedIndexBuilder {
index: Index,
directory_ids: HashMap<PathBuf, (EntryId, Arc<crate::control::ControlChain>)>,
repeated_directories: Vec<PathBuf>,
inserted: u64,
total_bytes: u64,
total_allocated: u64,
tree: Option<TreeFold>,
}
struct TreeFold {
retention: crate::execution::TreeRetention,
largest: std::collections::BinaryHeap<std::cmp::Reverse<KeptFile>>,
folded: Vec<FoldedFiles>,
}
struct KeptFile {
value: u64,
parent: EntryId,
name: OsString,
attrs: Attrs,
ignored: bool,
}
impl PartialEq for KeptFile {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl Eq for KeptFile {}
impl PartialOrd for KeptFile {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for KeptFile {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.value.cmp(&other.value)
}
}
impl TreeFold {
fn fold(folded: &mut Vec<FoldedFiles>, parent: EntryId, attrs: &Attrs, ignored: bool) {
let slot = parent.idx();
if folded.len() <= slot {
folded.resize(slot + 1, FoldedFiles::default());
}
folded[slot].add(attrs, ignored);
}
fn offer(&mut self, parent: EntryId, name: &mut OsString, attrs: Attrs, ignored: bool) {
let value = self.retention.size.of(&attrs);
let capacity = usize::try_from(self.retention.largest_files).unwrap_or(usize::MAX);
if self.largest.len() < capacity {
let name = std::mem::take(name);
self.largest.push(std::cmp::Reverse(KeptFile { value, parent, name, attrs, ignored }));
return;
}
match self.largest.peek_mut() {
Some(mut smallest) if value > smallest.0.value => {
let name = std::mem::take(name);
let displaced = std::mem::replace(
&mut smallest.0,
KeptFile { value, parent, name, attrs, ignored },
);
drop(smallest);
Self::fold(&mut self.folded, displaced.parent, &displaced.attrs, displaced.ignored);
}
_ => Self::fold(&mut self.folded, parent, &attrs, ignored),
}
}
}
impl DetachedIndexBuilder {
pub(crate) fn new(
root_path: impl Into<PathBuf>,
scope: ScanScope,
types: std::sync::Arc<crate::classify::TypeRegistry>,
) -> Self {
let mut index = Index::new_with_scope_and_types(root_path, scope, types);
index.entry_mut(EntryId::ROOT).directory_mut().children =
DirectoryChildren::Sorted(Vec::new());
Self {
index,
directory_ids: HashMap::from([(PathBuf::new(), (EntryId::ROOT, Arc::default()))]),
repeated_directories: Vec::new(),
inserted: 0,
total_bytes: 0,
total_allocated: 0,
tree: None,
}
}
pub(crate) fn with_control_limits(mut self, limits: crate::control::ControlLimits) -> Self {
self.index.set_control_limits(limits);
self
}
pub(crate) fn folding(mut self, retention: crate::execution::TreeRetention) -> Self {
self.tree = Some(TreeFold {
retention,
largest: std::collections::BinaryHeap::new(),
folded: Vec::new(),
});
self
}
pub(crate) fn push_directory(
&mut self,
directory: &mut crate::scan::DetachedDirectory,
) -> crate::Result<()> {
let crate::scan::DetachedDirectory { path, children, control } = directory;
let Some((parent, above)) = self.directory_ids.remove(path.as_path()) else {
if self.repeated_directories.iter().any(|repeated| path.starts_with(repeated)) {
return Ok(());
}
return Err(crate::Error::UnknownAncestry {
path: path.clone(),
reconcile_from: PathBuf::new(),
});
};
let listed_control = control.is_some();
if let Some(control) = control.take() {
debug_assert!(
matches!(&control, Op::ControlUpsert { path: control_path, .. }
| Op::ControlRemove { path: control_path }
if control_path.parent() == Some(path.as_path())),
"a listing carries only its own directory's control"
);
match control {
Op::ControlUpsert { path, source } => {
self.index.controls.upsert(&path, source)?;
}
Op::ControlRemove { path } => {
self.index.controls.remove(&path)?;
}
_ => unreachable!("detached directory retains only fixed-control operations"),
}
self.inserted = self.inserted.saturating_add(1);
}
children.sort_unstable_by(|left, right| {
left.name.cmp(&right.name).then(left.position.cmp(&right.position))
});
let repeated_directories = &mut self.repeated_directories;
children.dedup_by(|later, kept| {
if later.name != kept.name {
return false;
}
if later.kind.is_dir() || kept.kind.is_dir() {
let repeated = path.join(&kept.name);
if repeated_directories.last() != Some(&repeated) {
repeated_directories.push(repeated);
}
}
std::mem::swap(later, kept);
true
});
let parent_ignored = self.index.entry(parent).ignored;
let chain =
if listed_control { self.index.controls.chain_below(&above, path) } else { above };
debug_assert!(
chain.same_as(&self.index.controls.chain_for(path)),
"a derived control chain is the one the table resolves for {}",
path.display()
);
let classifying = !parent_ignored && !chain.is_empty();
let entries = match self.tree {
None => children.len(),
Some(_) => children.iter().filter(|child| child.kind != EntryKind::File).count(),
};
self.index.reserve_detached_children(parent, entries);
let path: &Path = path;
let mut classify_children = |directory: &[&[u8]]| -> crate::Result<()> {
for child in children.iter_mut() {
let &mut crate::scan::DetachedChild { ref mut name, kind, attrs, .. } = child;
crate::counters::bump(|counts| counts.upserts += 1);
if kind == EntryKind::File {
add_file_sizes(
&mut self.total_bytes,
&mut self.total_allocated,
&attrs,
|| path.join(&*name),
)?;
}
let ext_id = (kind == EntryKind::File && self.tree.is_none())
.then(|| self.index.intern_ext(&crate::classify::ext_bucket(name)));
let ignored = if classifying {
chain.is_ignored_within(directory, name.as_encoded_bytes(), kind.is_dir())
} else {
parent_ignored
};
if let (EntryKind::File, Some(tree)) = (kind, &mut self.tree) {
let direct = Index::file_contribution(&attrs, None, ignored);
crate::counters::bump(|counts| counts.rollup_merges += 1);
self.index.entry_mut(parent).rollup_mut().merge(&direct);
tree.offer(parent, name, attrs, ignored);
self.inserted = self.inserted.saturating_add(1);
continue;
}
let name = std::mem::take(name);
let child_path = kind.is_dir().then(|| path.join(&name));
let child_id = self.index.alloc(Entry::new_detached(
NewEntry {
parent: Some(parent),
name,
ext_id,
ignored,
source: Source::Scanned,
kind,
attrs,
},
true,
));
self.index.push_detached_child(parent, child_id);
let direct = self.index.contribution(child_id);
crate::counters::bump(|counts| counts.rollup_merges += 1);
self.index.entry_mut(parent).rollup_mut().merge(&direct);
if let Some(child_path) = child_path {
self.directory_ids.insert(child_path, (child_id, Arc::clone(&chain)));
}
self.inserted = self.inserted.saturating_add(1);
}
Ok(())
};
if classifying {
crate::control::with_directory_components(path, classify_children)?;
} else {
classify_children(&[])?;
}
if self.tree.is_none() {
children.clear();
}
Ok(())
}
fn keep_largest_files(&mut self, tree: TreeFold) {
let TreeFold { largest, folded, .. } = tree;
let mut gained = Vec::with_capacity(largest.len());
for std::cmp::Reverse(file) in largest {
let KeptFile { parent, name, attrs, ignored, .. } = file;
let id = self.index.alloc(Entry::new_detached(
NewEntry {
parent: Some(parent),
name,
ext_id: None,
ignored,
source: Source::Scanned,
kind: EntryKind::File,
attrs,
},
true,
));
self.index.push_detached_child(parent, id);
gained.push(parent.idx());
}
gained.sort_unstable();
gained.dedup();
for slot in gained {
let parent = EntryId {
slot: u32::try_from(slot).expect("index arena exceeded u32 capacity"),
generation: 0,
};
self.index.sort_detached_children(parent);
}
self.index.folded = Some(folded);
}
pub(crate) fn finish(mut self) -> Index {
if let Some(tree) = self.tree.take() {
self.keep_largest_files(tree);
}
for slot in (1..self.index.arena.len()).rev() {
let id = EntryId {
slot: u32::try_from(slot).expect("index arena exceeded u32 capacity"),
generation: 0,
};
if !self.index.entry(id).kind.is_dir() {
continue;
}
let parent = self.index.entry(id).parent.expect("every non-root entry has a parent");
crate::counters::bump(|counts| counts.rollup_merges += 1);
self.index.merge_detached_descendants(parent, id);
}
crate::counters::bump(|counts| {
counts.baseline_batches = counts.baseline_batches.saturating_add(1);
counts.baseline_accepted_ops =
counts.baseline_accepted_ops.saturating_add(self.inserted);
});
self.index.establish_baseline();
self.index
}
}
impl Index {
pub fn new(root_path: impl Into<PathBuf>) -> Self {
Self::new_with_scope(root_path, ScanScope::default())
}
pub fn new_with_scope(root_path: impl Into<PathBuf>, scope: ScanScope) -> Self {
Self::new_with_scope_and_types(
root_path,
scope,
crate::classify::TypeRegistry::compiled_shared(),
)
}
pub fn new_with_config(root_path: impl Into<PathBuf>, config: &crate::ScanConfig) -> Self {
let mut index =
Self::new_with_scope_and_types(root_path, config.scope(), config.types_shared());
index.set_control_limits(config.control_limits);
index
}
pub(crate) fn new_with_scope_and_types(
root_path: impl Into<PathBuf>,
scope: ScanScope,
types: std::sync::Arc<crate::classify::TypeRegistry>,
) -> Self {
Self::new_with_scope_types_and_journal_capacity_bytes(
root_path,
scope,
types,
DEFAULT_JOURNAL_CAPACITY_BYTES,
)
}
pub(crate) fn new_with_scope_types_and_journal_capacity_bytes(
root_path: impl Into<PathBuf>,
scope: ScanScope,
types: std::sync::Arc<crate::classify::TypeRegistry>,
journal_capacity_bytes: usize,
) -> Self {
assert_eq!(
scope.type_rules_fingerprint,
types.fingerprint(),
"an index's registry must match its semantic scope"
);
Self::new_with_journal_capacity_bytes(root_path, scope, journal_capacity_bytes, types, None)
}
pub(crate) fn new_opened_with_scope_types_and_journal_capacity_bytes(
root_path: impl Into<PathBuf>,
scope: ScanScope,
types: std::sync::Arc<crate::classify::TypeRegistry>,
journal_capacity_bytes: usize,
) -> Self {
assert_eq!(
scope.type_rules_fingerprint,
types.fingerprint(),
"an index's registry must match its semantic scope"
);
let serving = ServingIndexes::for_types(&types);
Self::new_with_journal_capacity_bytes(
root_path,
scope,
journal_capacity_bytes,
types,
Some(Box::new(serving)),
)
}
fn new_with_journal_capacity_bytes(
root_path: impl Into<PathBuf>,
scope: ScanScope,
journal_capacity_bytes: usize,
types: std::sync::Arc<crate::classify::TypeRegistry>,
serving: Option<Box<ServingIndexes>>,
) -> Self {
let root = Entry::new(
NewEntry {
parent: None,
name: OsString::new(),
ext_id: None,
ignored: false,
source: Source::Scanned,
kind: EntryKind::Dir,
attrs: Attrs::default(),
},
true,
);
let constructed_at_ns = Self::now_unix_nanos();
Self {
root_path: root_path.into(),
scope,
arena: vec![Slot::Occupied { generation: 0, entry: root }],
free_head: None,
live: 1,
clock: Clock::ZERO,
journal: VecDeque::new(),
journal_cost: 0,
journal_capacity_bytes,
journal_floor: Clock::ZERO,
pending_invalidations: Vec::new(),
freshness_epoch: 0,
freshness_marks: BTreeMap::new(),
state: IndexState::default(),
issues: Vec::new(),
issue_epochs: Vec::new(),
omitted_issue_epochs: BTreeMap::new(),
serving,
applying_source: Source::Scanned,
scanned_at_ns: constructed_at_ns,
captured_at_ns: 0,
writing_pass_started_at_ns: constructed_at_ns,
persistence_owed: true,
active_root_reconciles: BTreeMap::new(),
active_reconciles: BTreeMap::new(),
verified: Vec::new(),
ext_names: Vec::new(),
ext_ids: BTreeMap::new(),
ext_refcounts: Vec::new(),
free_ext_ids: Vec::new(),
content: None,
types,
controls: crate::control::ControlTable::default(),
unreadable_control_paths: BTreeSet::new(),
folded: None,
}
}
pub fn types(&self) -> &crate::classify::TypeRegistry {
self.types.as_ref()
}
pub fn controls(&self) -> crate::Result<&crate::control::ControlTable> {
self.require_observed_controls()?;
Ok(&self.controls)
}
pub const fn observes_controls(&self) -> bool {
self.scope.observes_controls()
}
pub fn control_identity(&self) -> crate::ControlTierIdentity {
if self.observes_controls() {
crate::ControlTierIdentity::Observed { limits: self.controls.limits() }
} else {
crate::ControlTierIdentity::NotObserved
}
}
pub fn snapshot_identity(&self) -> crate::SnapshotIdentity {
crate::SnapshotIdentity {
entries: crate::EntryTierIdentity::of_scope(self.scope),
controls: self.control_identity(),
}
}
fn require_observed_controls(&self) -> crate::Result<()> {
if self.observes_controls() { Ok(()) } else { Err(crate::Error::ControlStateNotObserved) }
}
fn carries_unobserved_control_input(&self, ops: &[ObservationOp]) -> bool {
!self.observes_controls()
&& ops.iter().any(|observed| {
matches!(observed.op, Op::ControlUpsert { .. } | Op::ControlRemove { .. })
})
}
pub(crate) fn control_table(&self) -> &crate::control::ControlTable {
&self.controls
}
pub fn control_coverage(&self) -> crate::control::ControlCoverage {
if self.observes_controls() {
crate::control::ControlCoverage::Observed(self.controls.observation())
} else {
crate::control::ControlCoverage::NotObserved
}
}
pub(crate) fn set_control_limits(&mut self, limits: crate::control::ControlLimits) {
debug_assert!(self.controls.is_vacant(), "the control limits are set before any control");
self.controls = crate::control::ControlTable::with_limits(limits);
}
pub(crate) fn require_control_limits_in_scope(
&self,
limits: crate::control::ControlLimits,
) -> crate::Result<()> {
if self.scope.observes_controls()
&& (crate::ControlTierIdentity::Observed { limits }).ignore_rules_fingerprint()
!= self.scope.ignore_rules_fingerprint
{
return Err(crate::Error::ControlLimitsOutsideScope { limits });
}
Ok(())
}
pub(crate) fn install_controls(
&mut self,
controls: crate::control::ControlTable,
) -> crate::Result<()> {
self.require_control_limits_in_scope(controls.limits())?;
if controls.limits().budget.is_some_and(|budget| controls.retained_cost() > budget) {
return Err(crate::Error::Snapshot(
"a snapshot's control table exceeds its own control budget".into(),
));
}
if !controls.is_vacant() {
self.require_observed_controls()?;
}
let unchanged = controls.is_empty() && self.controls.is_empty();
self.controls = controls;
if unchanged {
return Ok(());
}
let mut stats = ApplyStats::default();
let mut effects = NoConsequences;
self.reclassify_controlled_subtrees(&[PathBuf::new()], &mut stats, &mut effects);
Ok(())
}
pub(crate) fn types_shared(&self) -> std::sync::Arc<crate::classify::TypeRegistry> {
std::sync::Arc::clone(&self.types)
}
pub fn classify(&self, relative_path: &Path) -> crate::classify::Classification {
crate::classify::classify_with(&self.types, relative_path, None)
}
#[cfg(test)]
fn with_journal_capacity_bytes(
root_path: impl Into<PathBuf>,
journal_capacity_bytes: usize,
) -> Self {
Self::new_with_journal_capacity_bytes(
root_path,
ScanScope::default(),
journal_capacity_bytes,
crate::classify::TypeRegistry::compiled_shared(),
None,
)
}
pub fn root_path(&self) -> &Path {
&self.root_path
}
pub const fn scope(&self) -> ScanScope {
self.scope
}
pub fn freshness(&self) -> Freshness {
self.freshness_at(Path::new(""))
}
fn published_freshness(&self) -> Freshness {
let derived = self.freshness();
if self.state.phase == LifecyclePhase::Reconciling && derived == Freshness::Fresh {
Freshness::Reconciling
} else {
derived
}
}
pub(crate) const fn state(&self) -> IndexState {
self.state
}
#[allow(dead_code)] pub(crate) fn issues(&self) -> &[Issue] {
&self.issues
}
#[allow(dead_code)] pub(crate) fn directory_complete(&self, path: &Path) -> Option<bool> {
let id = self.lookup(path)?;
let entry = self.entry(id);
(entry.kind == EntryKind::Dir).then(|| entry.directory().children_complete)
}
pub fn freshness_at(&self, path: &Path) -> Freshness {
self.freshness_marks
.iter()
.filter(|(marked, _)| path.starts_with(marked) || marked.starts_with(path))
.map(|(_, mark)| mark.state)
.max_by_key(|state| state.rank())
.unwrap_or(Freshness::Fresh)
}
pub fn clock(&self) -> Clock {
self.clock
}
pub fn len(&self) -> u64 {
self.live
}
pub fn is_empty(&self) -> bool {
self.live <= 1
}
pub fn total(&self) -> RollUp {
self.named_rollup(&self.entry(EntryId::ROOT).rollup().all)
}
pub fn partition_total(&self) -> crate::Result<PartitionRollUp> {
self.require_observed_controls()?;
Ok(self.named_partitions(self.entry(EntryId::ROOT).rollup()))
}
pub(crate) fn total_scalars(&self) -> RollUpScalars {
RollUpScalars::from(&self.entry(EntryId::ROOT).rollup().all)
}
pub fn apply(&mut self, observation: &Observation) -> crate::Result<ApplyOutcome> {
let prepared = prepare_observation(observation)?;
let outcome = self.commit_prepared(prepared, true)?;
record_batch(BatchProvenance::Public, observation.len(), outcome.stats);
Ok(outcome)
}
fn commit_prepared(
&mut self,
prepared: PreparedObservation,
journal: bool,
) -> crate::Result<ApplyOutcome> {
self.commit_prepared_with(prepared, journal, None, None, None, false)
}
fn commit_prepared_with(
&mut self,
prepared: PreparedObservation,
journal: bool,
discovery: Option<DiscoveryCommit>,
observation: Option<ObservationTransition>,
max_files: Option<u64>,
track_file_progress: bool,
) -> crate::Result<ApplyOutcome> {
debug_assert!(!self.is_folded(), "a folded index answers one report and is never mutated");
if prepared.ops.is_empty()
&& discovery.as_ref().is_none_or(|discovery| {
discovery.directory_complete.is_none() && discovery.transition.is_none()
})
&& observation.is_none()
{
return Ok(ApplyOutcome::default());
}
if discovery.is_some()
&& matches!(self.state.phase, LifecyclePhase::Stopped | LifecyclePhase::Failed)
{
return Err(crate::Error::OpenedIndexStopped);
}
let mut discovery = discovery;
if let Some(path) =
discovery.as_mut().and_then(|discovery| discovery.directory_complete.as_mut())
{
let canonical = canonical_relative_path(path)?;
let Some(id) = self.lookup(&canonical) else {
return Err(crate::Error::InvalidDirectoryCompletion(canonical));
};
if self.entry(id).kind != EntryKind::Dir {
return Err(crate::Error::InvalidDirectoryCompletion(canonical));
}
*path = canonical;
}
#[cfg(test)]
if prepared.reject_before_apply {
return Err(crate::Error::CommitRejected("injected reducer preflight"));
}
let Some(next_clock) = self.clock.checked_next() else {
let mut probe = self.clone();
probe.clock = Clock(self.clock.0 - 1);
let outcome = probe.commit_prepared_with(
prepared,
false,
discovery,
observation,
max_files,
track_file_progress,
)?;
return if outcome.commit.is_some() {
Err(crate::Error::ClockExhausted)
} else {
Ok(outcome)
};
};
let observed = u64::try_from(prepared.ops.len()).unwrap_or(u64::MAX);
let mut effects = ExactConsequences::default();
let stats = self.reduce_prepared(
&prepared,
discovery,
observation,
max_files,
track_file_progress,
&mut effects,
)?;
if effects.is_empty() {
return Ok(ApplyOutcome::from_commit(stats, None));
}
let commit =
self.publish_effects(next_clock, effects, commit_work(observed, stats), journal);
Ok(ApplyOutcome::from_commit(stats, Some(commit)))
}
fn reduce_prepared<C: ConsequenceSink>(
&mut self,
prepared: &PreparedObservation,
discovery: Option<DiscoveryCommit>,
observation: Option<ObservationTransition>,
max_files: Option<u64>,
track_file_progress: bool,
effects: &mut C,
) -> crate::Result<ApplyStats> {
if self.carries_unobserved_control_input(&prepared.ops) {
return Err(crate::Error::ControlStateNotObserved);
}
if matches!(prepared.ancestry, PreparedAncestry::Scanner { .. }) {
debug_assert!(observation.is_none());
return self.reduce_scanner_prepared(
prepared,
discovery,
max_files,
track_file_progress,
effects,
);
}
let mut stats = ApplyStats::default();
let mut parent_memo = ParentMemo::default();
let accepted = self.accepted_operations(&prepared.ops);
stats.stale = u64::try_from(accepted.iter().filter(|accepted| !**accepted).count())
.unwrap_or(u64::MAX);
self.validate_known_ancestry(&prepared.ops, &accepted)?;
let projected_controls = self.projected_controls(&prepared.ops, &accepted)?;
self.preflight_totals(&prepared.ops, Some(&accepted), max_files)?;
for (observed, accepted) in prepared.ops.iter().zip(accepted) {
if !accepted {
continue;
}
let op = &observed.op;
if let (Some(max_files), Op::Upsert { path, kind, .. }) = (max_files, op) {
if self.files_after_upsert(path, *kind) > max_files {
stats.resource_refused = stats.resource_refused.saturating_add(1);
continue;
}
}
match op {
Op::Upsert { path, kind, attrs } => {
self.apply_upsert(path, *kind, *attrs, &mut stats, effects, &mut parent_memo);
}
Op::Remove { path } => {
parent_memo.clear();
self.apply_remove(path, &mut stats, effects);
}
Op::ControlUpsert { .. } | Op::ControlRemove { .. } => {
parent_memo.clear();
}
Op::InvalidateSubtree { path, reason } => {
parent_memo.clear();
let previous_index_state = self.state;
let previous = self.freshness_at(path);
self.pending_invalidations.push((path.clone(), *reason));
self.mark_unfresh(path, Freshness::Stale);
let current = self.freshness_at(path);
self.state.freshness = self.published_freshness();
if matches!(
reason,
InvalidateReason::WatchOverflow
| InvalidateReason::UnpairedRename
| InvalidateReason::WatchSetupRace
| InvalidateReason::VerificationFailed
| InvalidateReason::UnknownAncestry
| InvalidateReason::WatchContention
) {
self.retain_issue(Issue::observation_gap(path, *reason));
}
stats.invalidated += 1;
effects.change(|| EffectiveChange::Invalidated {
path: path.clone(),
reason: *reason,
});
if previous != current {
effects.state(|| StateTransition::Freshness {
path: path.clone(),
previous,
current,
});
}
if previous_index_state != self.state {
effects.state(|| StateTransition::IndexState {
previous: previous_index_state,
current: self.state,
});
}
}
}
}
self.finish_reduction(
projected_controls,
&mut stats,
discovery,
observation,
max_files,
track_file_progress,
effects,
);
Ok(stats)
}
fn reduce_scanner_prepared<C: ConsequenceSink>(
&mut self,
prepared: &PreparedObservation,
discovery: Option<DiscoveryCommit>,
max_files: Option<u64>,
track_file_progress: bool,
effects: &mut C,
) -> crate::Result<ApplyStats> {
let PreparedAncestry::Scanner { parents, has_batch_parents } = &prepared.ancestry else {
unreachable!("scanner reduction requires scanner ancestry");
};
debug_assert_eq!(prepared.ops.len(), parents.len());
let projection_started = crate::counters::enabled().then(std::time::Instant::now);
let projected_controls =
self.projected_controls_from(prepared.ops.iter().map(|observed| &observed.op))?;
if let Some(started) = projection_started {
let elapsed = elapsed_micros(started);
crate::counters::bump(|counts| {
counts.scanner_control_projection_us =
counts.scanner_control_projection_us.saturating_add(elapsed);
});
}
self.preflight_totals(&prepared.ops, None, max_files)?;
let mut stats = ApplyStats::default();
let mut applied_ids = has_batch_parents.then(|| vec![None; prepared.ops.len()]);
for (op_index, (observed, parent)) in prepared.ops.iter().zip(parents.iter()).enumerate() {
match &observed.op {
Op::Upsert { path, kind, attrs } => {
if let Some(max_files) = max_files {
if self.files_after_upsert(path, *kind) > max_files {
stats.resource_refused = stats.resource_refused.saturating_add(1);
continue;
}
}
let parent = match parent {
ResolvedParent::Existing(parent) => *parent,
ResolvedParent::Earlier(parent_op) => applied_ids
.as_ref()
.and_then(|ids| ids.get(*parent_op))
.copied()
.flatten()
.expect("a proved parent directory was applied earlier"),
};
let name = path.file_name().expect("scanner upserts are not root mutations");
crate::counters::bump(|counts| counts.upserts += 1);
self.upsert_beneath(parent, name, path, *kind, *attrs, &mut stats, effects);
if kind.is_dir() {
if let Some(ids) = &mut applied_ids {
ids[op_index] = self.child(parent, name);
}
}
}
Op::ControlUpsert { .. } | Op::ControlRemove { .. } => {}
Op::Remove { .. } | Op::InvalidateSubtree { .. } => {
unreachable!("scanner preparation rejects non-discovery operations");
}
}
}
self.finish_reduction(
projected_controls,
&mut stats,
discovery,
None,
max_files,
track_file_progress,
effects,
);
Ok(stats)
}
#[allow(clippy::too_many_arguments)] fn finish_reduction<C: ConsequenceSink>(
&mut self,
projected_controls: Option<crate::control::ControlTable>,
stats: &mut ApplyStats,
discovery: Option<DiscoveryCommit>,
observation: Option<ObservationTransition>,
max_files: Option<u64>,
track_file_progress: bool,
effects: &mut C,
) {
self.apply_control_transition(projected_controls, stats, effects);
let mut discovery = discovery;
if stats.resource_refused > 0 {
let max_files = max_files.expect("resource refusal requires a file limit");
let discovery = discovery.get_or_insert_with(DiscoveryCommit::default);
discovery.directory_complete = None;
discovery.transition =
Some(DiscoveryTransition::BudgetRefused(Issue::resource_budget(max_files)));
}
self.apply_opened_state(discovery, observation, track_file_progress, effects);
}
fn apply_opened_state<C: ConsequenceSink>(
&mut self,
discovery: Option<DiscoveryCommit>,
observation: Option<ObservationTransition>,
track_file_progress: bool,
effects: &mut C,
) {
if discovery.is_none() && observation.is_none() && !track_file_progress {
return;
}
let previous = self.state;
if track_file_progress {
self.state.progress.files_retained = self.total_scalars().files;
}
if let Some(discovery) = discovery {
if let Some(path) = discovery.directory_complete {
let id = self.lookup(&path).expect("discovery completion was preflighted");
if !self.entry(id).directory().children_complete {
self.entry_mut(id).directory_mut().children_complete = true;
self.state.progress.directories_complete =
self.state.progress.directories_complete.saturating_add(1);
effects.state(|| StateTransition::DirectoryComplete { path });
}
}
if let Some(transition) = discovery.transition {
match transition {
DiscoveryTransition::Begin => {
for slot in &mut self.arena {
if let Slot::Occupied { entry, .. } = slot {
if entry.kind == EntryKind::Dir {
entry.directory_mut().children_complete = false;
}
}
}
self.state = IndexState {
phase: LifecyclePhase::Discovering,
coverage: Coverage::Partial(CoverageReason::Building),
freshness: Freshness::Fresh,
source: Source::Scanned,
progress: DiscoveryProgress::default(),
issues: crate::IssueSummary::default(),
};
self.issues.clear();
self.issue_epochs.clear();
self.omitted_issue_epochs.clear();
}
DiscoveryTransition::Finish => {
self.state.phase = LifecyclePhase::Ready;
if self.state.coverage == Coverage::Partial(CoverageReason::Building) {
self.state.coverage = Coverage::Complete;
}
self.state.freshness = if self.state.coverage == Coverage::Complete {
Freshness::Fresh
} else {
Freshness::Partial
};
}
DiscoveryTransition::BudgetRefused(issue) => {
let already_stopped_for_budget = self.state.phase
== LifecyclePhase::Stopped
&& self.state.coverage == Coverage::Partial(CoverageReason::Budget);
self.state.phase = LifecyclePhase::Stopped;
self.state.coverage = Coverage::Partial(CoverageReason::Budget);
self.state.freshness = Freshness::Fresh;
if !already_stopped_for_budget {
self.retain_issue(issue);
}
}
DiscoveryTransition::Inaccessible { issues, omitted } => {
if self.state.coverage != Coverage::Partial(CoverageReason::Budget) {
self.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
self.state.freshness = Freshness::Partial;
}
for issue in issues {
self.retain_issue(issue);
}
self.retain_omitted(omitted);
}
DiscoveryTransition::Cancelled => {
self.state.phase = LifecyclePhase::Stopped;
if self.state.coverage != Coverage::Complete {
self.state.coverage = Coverage::Partial(CoverageReason::Cancelled);
}
}
DiscoveryTransition::Failed(issue) => {
self.state.phase = LifecyclePhase::Failed;
self.state.coverage = Coverage::Partial(CoverageReason::Failed);
self.state.freshness = Freshness::Partial;
self.retain_issue(issue);
}
}
}
}
if let Some(observation) = observation {
match observation {
ObservationTransition::Reconciling => {
if self.state.phase == LifecyclePhase::Ready {
self.state.phase = LifecyclePhase::Reconciling;
self.state.freshness = Freshness::Reconciling;
}
}
ObservationTransition::Watching { issues, omitted } => {
if self.state.phase == LifecyclePhase::Reconciling {
self.state.phase = LifecyclePhase::Watching;
if !issues.is_empty() || omitted > 0 {
self.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
for issue in issues {
self.retain_issue(issue);
}
self.retain_omitted(omitted);
} else if self.state.coverage
== Coverage::Partial(CoverageReason::Inaccessible)
{
self.state.coverage = Coverage::Complete;
}
self.state.freshness = self.freshness();
if self.state.coverage != Coverage::Complete {
self.state.freshness = Freshness::Partial;
}
}
}
ObservationTransition::Unreadable { issues, omitted } => {
if matches!(self.state.phase, LifecyclePhase::Watching | LifecyclePhase::Ready)
{
for issue in issues {
self.retain_issue(issue);
}
self.retain_omitted(omitted);
}
}
ObservationTransition::Failed(issue) => {
if self.state.phase != LifecyclePhase::Stopped {
self.state.phase = LifecyclePhase::Failed;
self.state.freshness = Freshness::Partial;
self.retain_issue(issue);
}
}
}
}
if previous != self.state {
effects.state(|| StateTransition::IndexState { previous, current: self.state });
}
}
fn subtree_scalars(&self, id: EntryId) -> RollUpScalars {
let entry = self.entry(id);
match entry.kind {
EntryKind::Dir => {
let mut scalars = RollUpScalars::from(&entry.rollup().all);
scalars.dirs = scalars.dirs.saturating_add(1);
scalars
}
EntryKind::File => RollUpScalars::leaf(EntryKind::File, &entry.attrs),
EntryKind::Symlink | EntryKind::Other => RollUpScalars::default(),
}
}
fn preflight_totals(
&self,
ops: &[ObservationOp],
accepted: Option<&[bool]>,
max_files: Option<u64>,
) -> crate::Result<()> {
let mut bound = self.total_scalars();
for (index, observed) in ops.iter().enumerate() {
if accepted.is_some_and(|accepted| !accepted[index]) {
continue;
}
if let Op::Upsert { kind, attrs, .. } = &observed.op {
match bound.checked_add(&RollUpScalars::leaf(*kind, attrs)) {
Ok(next) => bound = next,
Err(_) => return self.replay_totals(ops, accepted, max_files),
}
}
}
Ok(())
}
fn replay_totals(
&self,
ops: &[ObservationOp],
accepted: Option<&[bool]>,
max_files: Option<u64>,
) -> crate::Result<()> {
let mut total = self.total_scalars();
let mut overlay = TotalsOverlay::default();
for (index, observed) in ops.iter().enumerate() {
if accepted.is_some_and(|accepted| !accepted[index]) {
continue;
}
match &observed.op {
Op::Upsert { path, kind, attrs } => {
if path.as_os_str().is_empty()
|| (overlay.kind_at(self, path) == Some(*kind) && *kind != EntryKind::File)
{
continue;
}
let old = overlay.subtree_at(self, path);
let new = RollUpScalars::leaf(*kind, attrs);
let after =
total.saturating_sub(&old).checked_add(&new).map_err(|counter| {
crate::Error::UnrepresentableTotal { path: path.clone(), counter }
})?;
if max_files.is_some_and(|max_files| after.files > max_files) {
continue;
}
overlay.replace(self, path, *kind, new);
total = after;
}
Op::Remove { path } => {
if path.as_os_str().is_empty() {
continue;
}
let old = overlay.subtree_at(self, path);
overlay.cut(self, path);
total = total.saturating_sub(&old);
}
Op::ControlUpsert { .. }
| Op::ControlRemove { .. }
| Op::InvalidateSubtree { .. } => {}
}
}
Ok(())
}
fn files_after_upsert(&self, path: &Path, kind: EntryKind) -> u64 {
let current_total = self.total_scalars().files;
let Some(id) = self.lookup(path) else {
return current_total.saturating_add(u64::from(kind == EntryKind::File));
};
let current = self.entry(id);
if current.kind == kind {
return current_total;
}
let removed = match current.kind {
EntryKind::File => 1,
EntryKind::Dir => current.rollup().all.files,
_ => 0,
};
current_total.saturating_sub(removed).saturating_add(u64::from(kind == EntryKind::File))
}
fn retain_issue(&mut self, issue: Issue) {
self.retain_issue_at(issue, self.freshness_epoch);
}
fn retain_issue_at(&mut self, issue: Issue, epoch: u64) {
let repeat = self.issues.iter().position(|retained| same_issue_cause(retained, &issue));
if let Some(position) = repeat {
self.issue_epochs[position] = epoch;
} else {
let position = self
.issues
.binary_search_by(|retained| compare_issues(retained, &issue))
.unwrap_or_else(|position| position);
if position < MAX_RETAINED_ISSUES {
self.issues.insert(position, issue);
self.issue_epochs.insert(position, epoch);
if self.issues.len() > MAX_RETAINED_ISSUES {
self.issues.pop();
let omitted_epoch =
self.issue_epochs.pop().expect("issue epochs stay parallel");
self.retain_omitted_at(1, omitted_epoch);
}
} else {
self.retain_omitted_at(1, epoch);
}
self.state.issues.retained = u64::try_from(self.issues.len()).unwrap_or(u64::MAX);
}
}
fn retain_omitted(&mut self, count: u64) {
if count == 0 {
return;
}
let epoch =
self.active_reconciles.keys().next_back().copied().unwrap_or(self.freshness_epoch);
self.retain_omitted_at(count, epoch);
}
fn retain_omitted_at(&mut self, count: u64, epoch: u64) {
let retained = self.omitted_issue_epochs.entry(epoch).or_default();
*retained = retained.saturating_add(count);
self.state.issues.omitted = self.state.issues.omitted.saturating_add(count);
}
fn drop_disproven_omitted(&mut self, started_at: u64) {
self.omitted_issue_epochs.retain(|epoch, _| *epoch >= started_at);
self.state.issues.omitted =
self.omitted_issue_epochs.values().fold(0_u64, |sum, count| sum.saturating_add(*count));
}
fn compact_omitted_epochs(&mut self) {
let mut compact = BTreeMap::new();
for (epoch, count) in std::mem::take(&mut self.omitted_issue_epochs) {
let owner =
self.active_reconciles.range(..=epoch).next_back().map_or(0, |(epoch, _)| *epoch);
let retained = compact.entry(owner).or_insert(0_u64);
*retained = retained.saturating_add(count);
}
self.omitted_issue_epochs = compact;
}
fn drop_disproven_issues(&mut self, path: &Path, started_at: u64) {
let mut position = 0;
while position < self.issues.len() {
let issue = &self.issues[position];
let disproven = issue.kind != crate::IssueKind::ObservationGap
&& issue.path.as_deref().is_some_and(|issue_path| issue_path.starts_with(path))
&& self.issue_epochs[position] < started_at;
if disproven {
self.issues.remove(position);
self.issue_epochs.remove(position);
} else {
position += 1;
}
}
self.state.issues.retained = u64::try_from(self.issues.len()).unwrap_or(u64::MAX);
}
fn publish_effects(
&mut self,
next_clock: Clock,
effects: ExactConsequences,
work: Work,
journal: bool,
) -> Commit {
debug_assert!(!effects.is_empty());
if crate::counters::enabled() {
let effect_paths = u64::try_from(effects.changes.len()).unwrap_or(u64::MAX);
let effect_path_bytes = effects.changes.iter().fold(0_u64, |total, change| {
total.saturating_add(
u64::try_from(change.path().as_os_str().as_encoded_bytes().len())
.unwrap_or(u64::MAX),
)
});
crate::counters::bump(|counts| {
counts.effect_paths = counts.effect_paths.saturating_add(effect_paths);
counts.effect_path_bytes =
counts.effect_path_bytes.saturating_add(effect_path_bytes);
});
}
let commit = Commit {
clock: next_clock,
impact: derive_impact(&effects.changes, &effects.state),
changes: effects.changes,
state: effects.state,
work,
};
self.clock = next_clock;
if journal {
self.retain_commit(&commit);
}
commit
}
fn retain_commit(&mut self, commit: &Commit) {
let cost = commit.retained_cost();
if cost > self.journal_capacity_bytes {
let dropped = u64::try_from(self.journal.len()).unwrap_or(u64::MAX);
crate::counters::bump(|counts| {
counts.journal_oversized_commits =
counts.journal_oversized_commits.saturating_add(1);
counts.journal_dropped_commits =
counts.journal_dropped_commits.saturating_add(dropped);
});
self.journal.clear();
self.journal_cost = 0;
self.journal_floor = commit.clock;
return;
}
while self.journal_cost + cost > self.journal_capacity_bytes {
if let Some(dropped) = self.journal.pop_front() {
crate::counters::bump(|counts| {
counts.journal_dropped_commits =
counts.journal_dropped_commits.saturating_add(1);
});
self.journal_cost -= dropped.retained_cost();
self.journal_floor = dropped.clock;
}
}
self.journal_cost += cost;
self.journal.push_back(commit.clone());
crate::counters::bump(|counts| {
counts.journal_cloned_commits = counts.journal_cloned_commits.saturating_add(1);
counts.journal_retained_commits = counts.journal_retained_commits.saturating_add(1);
});
}
pub(crate) fn apply_baseline(
&mut self,
observation: &Observation,
) -> crate::Result<ApplyStats> {
let prepared = prepare_observation(observation)?;
#[cfg(test)]
if prepared.reject_before_apply {
return Err(crate::Error::CommitRejected("injected reducer preflight"));
}
let mut effects = NoConsequences;
let stats = self.reduce_prepared(&prepared, None, None, None, false, &mut effects)?;
record_batch(BatchProvenance::Baseline, observation.len(), stats);
self.establish_baseline();
Ok(stats)
}
pub(crate) fn apply_scanner_baseline(
&mut self,
batch: crate::scan::ScannerBatch,
) -> crate::Result<ApplyStats> {
let observed = batch.len();
let prepare_started = crate::counters::enabled().then(std::time::Instant::now);
let prepared = self.prepare_scanner_batch(batch)?;
if let Some(started) = prepare_started {
let elapsed = elapsed_micros(started);
crate::counters::bump(|counts| {
counts.scanner_prepare_us = counts.scanner_prepare_us.saturating_add(elapsed);
});
}
let mut effects = NoConsequences;
let reduce_started = crate::counters::enabled().then(std::time::Instant::now);
let stats = self.reduce_prepared(&prepared, None, None, None, false, &mut effects)?;
if let Some(started) = reduce_started {
let elapsed = elapsed_micros(started);
crate::counters::bump(|counts| {
counts.scanner_reduce_us = counts.scanner_reduce_us.saturating_add(elapsed);
});
}
record_batch(BatchProvenance::Baseline, observed, stats);
self.establish_baseline();
Ok(stats)
}
#[cfg(test)]
pub(crate) fn apply_ok(&mut self, observation: &Observation) -> ApplyOutcome {
self.apply(observation).expect("test observation must be valid")
}
#[cfg(test)]
pub(crate) fn apply_baseline_ok(&mut self, observation: &Observation) -> ApplyStats {
self.apply_baseline(observation).expect("test baseline must be valid")
}
pub(crate) fn establish_baseline(&mut self) {
self.clock = Clock::ZERO;
self.journal.clear();
self.journal_cost = 0;
self.journal_floor = Clock::ZERO;
self.pending_invalidations.clear();
}
pub(crate) fn mark_unverified(&mut self) {
self.freshness_marks.clear();
self.mark_unfresh(Path::new(""), Freshness::Stale);
self.state.source = Source::Cached;
self.state.freshness = Freshness::Stale;
}
pub(crate) fn set_initial_freshness(&mut self, complete: bool) {
if complete {
for slot in &mut self.arena {
if let Slot::Occupied { entry, .. } = slot {
if entry.kind == EntryKind::Dir {
entry.directory_mut().children_complete = true;
}
}
}
}
self.set_initial_state(complete);
}
pub(crate) fn set_initial_detached_scan_freshness(&mut self, errors: &[crate::Error]) {
if !errors.is_empty() {
self.set_initial_scan_freshness(errors);
return;
}
debug_assert!(
self.arena.iter().all(|slot| match slot {
Slot::Occupied { entry, .. } if entry.kind.is_dir() => {
entry.directory().children_complete
}
Slot::Occupied { .. } | Slot::Free { .. } => true,
}),
"a detached builder allocates every directory as listed in full"
);
self.set_initial_state(true);
}
fn set_initial_state(&mut self, complete: bool) {
self.freshness_marks.clear();
if complete {
self.state.phase = LifecyclePhase::Ready;
self.state.coverage = Coverage::Complete;
self.state.freshness = Freshness::Fresh;
} else {
self.mark_unfresh(Path::new(""), Freshness::Partial);
self.state.phase = LifecyclePhase::Ready;
self.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
self.state.freshness = Freshness::Partial;
}
}
pub(crate) fn set_initial_scan_freshness(&mut self, errors: &[crate::Error]) {
self.set_initial_freshness(errors.is_empty());
if errors.is_empty() {
return;
}
for slot in &mut self.arena {
if let Slot::Occupied { entry, .. } = slot {
if entry.kind.is_dir() {
entry.directory_mut().children_complete = false;
}
}
}
let mut failed = Vec::with_capacity(errors.len());
for error in errors {
let Some(path) = Issue::from_error_under(&self.root_path, error).path else {
return;
};
if path.is_absolute() || path.as_os_str().is_empty() {
return;
}
failed.push(path);
}
failed.sort();
failed.dedup();
let listings: Vec<_> =
failed.iter().map(|path| self.failed_listing_boundary(path)).collect();
self.freshness_marks.clear();
for path in &failed {
self.mark_unfresh(path, Freshness::Partial);
}
for slot in 0..self.arena.len() {
let Slot::Occupied { generation, entry } = &self.arena[slot] else {
continue;
};
if !entry.kind.is_dir() {
continue;
}
let id = EntryId {
slot: u32::try_from(slot).expect("index arena exceeded u32 capacity"),
generation: *generation,
};
let Some(path) = self.path_of(id) else {
continue;
};
self.entry_mut(id).directory_mut().children_complete =
!listings.iter().zip(&failed).any(|(boundary, failure)| {
path == *boundary || (boundary == failure && path.starts_with(boundary))
});
}
}
fn failed_listing_boundary(&self, failed: &Path) -> PathBuf {
let mut boundary = failed.to_path_buf();
loop {
if self.lookup(&boundary).is_some_and(|id| self.entry(id).kind.is_dir()) {
return boundary;
}
if !boundary.pop() {
return PathBuf::new();
}
}
}
pub(crate) fn record_walk_errors(&mut self, errors: &mut Vec<crate::Error>) {
self.issues.clear();
self.issue_epochs.clear();
self.omitted_issue_epochs.clear();
self.state.issues = crate::IssueSummary::default();
let root = self.root_path.clone();
crate::scan::normalize_walk_errors(&root, errors);
self.unreadable_control_paths.clear();
for error in errors {
if let Some(path) = crate::control::unreadable_control(&root, error) {
self.unreadable_control_paths.insert(path);
}
self.retain_issue(Issue::from_error_under(&root, error));
}
}
pub(crate) fn begin_reconcile(&mut self, path: &Path) -> crate::Result<(u64, Option<Commit>)> {
let path = canonical_relative_path(path)?;
let next_clock = self.clock.checked_next().ok_or(crate::Error::ClockExhausted)?;
let previous_index_state = self.state;
let previous = self.freshness_at(&path);
let epoch = self.mark_unfresh(&path, Freshness::Reconciling);
self.active_reconciles.insert(
epoch,
ActiveReconcile {
path: path.clone(),
scope_budget: usize::try_from(self.len()).unwrap_or(usize::MAX),
evidence: ReconcileEvidence::Scopes(BTreeSet::new()),
},
);
if path.as_os_str().is_empty() {
self.active_root_reconciles.insert(epoch, Self::now_unix_nanos());
}
let current = self.freshness_at(&path);
self.state.freshness = self.published_freshness();
let commit = if previous == current && previous_index_state == self.state {
None
} else {
let mut state = Vec::new();
if previous != current {
state.push(StateTransition::Freshness { path, previous, current });
}
if previous_index_state != self.state {
state.push(StateTransition::IndexState {
previous: previous_index_state,
current: self.state,
});
}
let effects = ExactConsequences { state, ..ExactConsequences::default() };
Some(self.publish_effects(next_clock, effects, Work::default(), true))
};
Ok((epoch, commit))
}
pub(crate) fn finish_reconcile(
&mut self,
path: &Path,
started_at: u64,
complete: bool,
listed_incomplete: &[PathBuf],
failed_paths: &[PathBuf],
errors: ReconcileErrors<'_>,
) -> crate::Result<ReconcileFinish> {
let path = canonical_relative_path(path)?;
let next_clock = self.clock.checked_next().ok_or(crate::Error::ClockExhausted)?;
let previous_index_state = self.state;
let previous = self.freshness_at(&path);
let evidence = self.active_reconciles.get(&started_at).map_or_else(
|| ReconcileEvidence::Scopes(BTreeSet::new()),
|active| active.evidence.clone(),
);
let superseded = match evidence {
ReconcileEvidence::Scopes(scopes) => scopes,
ReconcileEvidence::Retry => {
self.active_root_reconciles.remove(&started_at);
self.active_reconciles.remove(&started_at);
self.compact_omitted_epochs();
self.mark_unfresh(&path, Freshness::Partial);
if self.state.coverage == Coverage::Complete {
self.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
}
self.state.freshness = self.published_freshness();
self.retain_issue(Issue::provider_failure(
Some(&path),
"reconciliation interrupted by newer verification; retry this scope"
.to_string(),
));
let current = self.freshness_at(&path);
let mut state = Vec::new();
if previous != current {
state.push(StateTransition::Freshness { path, previous, current });
}
if previous_index_state != self.state {
state.push(StateTransition::IndexState {
previous: previous_index_state,
current: self.state,
});
}
let commit = if state.is_empty() {
None
} else {
let effects = ExactConsequences { state, ..ExactConsequences::default() };
Some(self.publish_effects(next_clock, effects, Work::default(), true))
};
return Ok(ReconcileFinish { commit, retry: true });
}
};
let fully_superseded = superseded.iter().any(|newer| path.starts_with(newer));
if errors.disproves_old && !fully_superseded {
for (epoch, active) in &mut self.active_reconciles {
if *epoch < started_at
&& (active.path.starts_with(&path) || path.starts_with(&active.path))
{
active.supersede(&path);
}
}
}
self.freshness_marks
.retain(|marked, mark| !marked.starts_with(&path) || mark.epoch > started_at);
if fully_superseded {
self.active_root_reconciles.remove(&started_at);
self.active_reconciles.remove(&started_at);
self.compact_omitted_epochs();
let current = self.freshness_at(&path);
self.state.freshness = self.published_freshness();
if self.state.coverage == Coverage::Partial(CoverageReason::Inaccessible) {
self.state.freshness = Freshness::Partial;
}
let mut state = Vec::new();
if previous != current {
state.push(StateTransition::Freshness { path, previous, current });
}
if previous_index_state != self.state {
state.push(StateTransition::IndexState {
previous: previous_index_state,
current: self.state,
});
}
if state.is_empty() {
return Ok(ReconcileFinish { commit: None, retry: false });
}
let effects = ExactConsequences { state, ..ExactConsequences::default() };
return Ok(ReconcileFinish {
commit: Some(self.publish_effects(next_clock, effects, Work::default(), true)),
retry: false,
});
}
let still_owned =
|candidate: &Path| !superseded.iter().any(|newer| candidate.starts_with(newer));
let verified_control = crate::control::governing_control(&path);
let in_scope = |control: &Path| {
control.starts_with(&path) || verified_control.as_deref() == Some(control)
};
if errors.disproves_old {
self.unreadable_control_paths
.retain(|control| !in_scope(control) || !still_owned(control));
}
for error in errors.errors.iter().chain(errors.terminal) {
if let Some(control) = crate::control::unreadable_control(&self.root_path, error) {
if in_scope(&control) && still_owned(&control) {
self.unreadable_control_paths.insert(control);
}
}
}
let recordable: Vec<&PathBuf> = listed_incomplete
.iter()
.filter(|directory| {
directory.starts_with(&path)
&& still_owned(directory)
&& !self.freshness_marks.iter().any(|(marked, mark)| {
mark.epoch > started_at && directory.starts_with(marked)
})
})
.collect();
let scoped_failures: Vec<&PathBuf> = failed_paths
.iter()
.filter(|failed| failed.starts_with(&path) && still_owned(failed))
.collect();
let complete = complete
|| (errors.disproves_old
&& failed_paths.iter().any(|failed| failed.starts_with(&path))
&& scoped_failures.is_empty()
&& errors.errors.iter().chain(errors.terminal).all(|error| {
Issue::from_error_under(&self.root_path, error)
.path
.is_some_and(|failed| !failed.starts_with(&path) || !still_owned(&failed))
}));
if errors.disproves_old && self.state.phase != LifecyclePhase::Failed {
self.drop_disproven_issues(&path, started_at);
}
if errors.disproves_old
&& self.state.phase != LifecyclePhase::Failed
&& path.as_os_str().is_empty()
{
self.drop_disproven_omitted(started_at);
}
let mut state = Vec::new();
if !complete && (!errors.errors.is_empty() || errors.terminal.is_some()) {
let boundaries: Vec<_> = if scoped_failures.is_empty() {
vec![(path.clone(), true)]
} else {
scoped_failures
.iter()
.map(|failed| {
let boundary = self.failed_listing_boundary(failed);
let subtree = boundary == **failed;
(boundary, subtree)
})
.collect()
};
for slot in 0..self.arena.len() {
let Slot::Occupied { generation, entry } = &self.arena[slot] else {
continue;
};
if !entry.kind.is_dir() || !entry.directory().children_complete {
continue;
}
let id = EntryId {
slot: u32::try_from(slot).expect("index arena exceeded u32 capacity"),
generation: *generation,
};
let Some(directory) = self.path_of(id) else {
continue;
};
if boundaries.iter().any(|(boundary, subtree)| {
directory == *boundary || (*subtree && directory.starts_with(boundary))
}) && still_owned(&directory)
&& !self.freshness_marks.iter().any(|(marked, mark)| {
mark.epoch > started_at && directory.starts_with(marked)
})
{
self.entry_mut(id).directory_mut().children_complete = false;
state.push(StateTransition::DirectoryIncomplete { path: directory });
}
}
}
let verified_scope = superseded.is_empty()
|| (complete
&& superseded.iter().all(|newer| self.freshness_at(newer) == Freshness::Fresh));
if verified_scope && (complete || !scoped_failures.is_empty()) {
let now = Self::now_unix_nanos();
self.verified.retain(|(verified_path, _)| !verified_path.starts_with(&path));
self.verified.push((path.clone(), now));
if self.verified.len() > MAX_VERIFIED_INTERVALS {
let excess = self.verified.len() - MAX_VERIFIED_INTERVALS;
self.verified.sort_by_key(|(_, at)| *at);
self.verified.drain(..excess);
}
state.push(StateTransition::Verified { path: path.clone() });
}
if complete {
if path.as_os_str().is_empty() {
if let Some(started) = self.active_root_reconciles.remove(&started_at) {
self.writing_pass_started_at_ns = started;
}
self.state.source = self.applying_source;
}
} else {
if scoped_failures.is_empty() {
self.mark_unfresh(&path, Freshness::Partial);
} else {
for failed in scoped_failures {
self.mark_unfresh(failed, Freshness::Partial);
}
}
if !errors.errors.is_empty() || errors.terminal.is_some() {
self.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
}
if path.as_os_str().is_empty() {
if let Some(started) = self.active_root_reconciles.remove(&started_at) {
self.writing_pass_started_at_ns = started;
}
self.state.source = self.applying_source;
}
}
for error in errors.errors.iter().chain(errors.terminal) {
let issue = Issue::from_error_under(&self.root_path, error);
if issue
.path
.as_deref()
.is_none_or(|issue_path| issue_path.starts_with(&path) && still_owned(issue_path))
{
self.retain_issue(issue);
}
}
for directory in recordable {
let Some(id) = self.lookup(directory) else {
continue;
};
let entry = self.entry_mut(id);
if entry.kind != EntryKind::Dir || entry.directory().children_complete {
continue;
}
entry.directory_mut().children_complete = true;
self.state.progress.directories_complete =
self.state.progress.directories_complete.saturating_add(1);
state.push(StateTransition::DirectoryComplete { path: directory.clone() });
}
if complete
&& self.state.coverage == Coverage::Partial(CoverageReason::Inaccessible)
&& !self.issues.iter().any(|issue| issue.kind != crate::IssueKind::ObservationGap)
&& self.state.issues.omitted == 0
&& self.arena.iter().all(|slot| {
let Slot::Occupied { entry, .. } = slot else {
return true;
};
entry.kind != EntryKind::Dir || entry.directory().children_complete
})
{
self.state.coverage = Coverage::Complete;
}
let current = self.freshness_at(&path);
self.state.freshness = self.published_freshness();
if self.state.coverage == Coverage::Partial(CoverageReason::Inaccessible) {
self.state.freshness = Freshness::Partial;
}
self.active_reconciles.remove(&started_at);
self.compact_omitted_epochs();
if previous != current {
state.push(StateTransition::Freshness { path: path.clone(), previous, current });
}
if previous_index_state != self.state {
state.push(StateTransition::IndexState {
previous: previous_index_state,
current: self.state,
});
}
if state.is_empty() {
return Ok(ReconcileFinish { commit: None, retry: false });
}
let effects = ExactConsequences { state, ..ExactConsequences::default() };
Ok(ReconcileFinish {
commit: Some(self.publish_effects(next_clock, effects, Work::default(), true)),
retry: false,
})
}
fn verified_at(&self, path: &Path) -> Option<i64> {
self.verified
.iter()
.filter(|(covered, _)| path.starts_with(covered))
.map(|(_, at)| *at)
.max()
}
fn mark_unfresh(&mut self, path: &Path, state: Freshness) -> u64 {
self.freshness_epoch =
self.freshness_epoch.checked_add(1).expect("freshness epoch exhausted");
let epoch = self.freshness_epoch;
self.freshness_marks.insert(path.to_path_buf(), FreshnessMark { state, epoch });
epoch
}
pub fn path_state(&self, path: &Path) -> PathState {
let Some(id) = self.lookup(path) else {
return PathState::Absent;
};
let entry = self.entry(id);
PathState::Present { kind: entry.kind, attrs: entry.attrs }
}
pub fn expectation(&self, path: &Path) -> PathExpectation {
let entry = self.entry_identity(path);
PathExpectation::new(
self.path_state(path),
entry,
entry.is_none().then(|| self.absence_guard_identity(path)).flatten(),
)
}
pub(crate) fn relaxed_expectation(&self, path: &Path) -> PathExpectation {
PathExpectation::new(self.path_state(path), self.entry_identity(path), None)
}
pub fn since(&self, clock: Clock) -> Since {
let commits: Vec<Commit> =
self.journal.iter().filter(|commit| commit.clock > clock).cloned().collect();
Since {
commits,
clock: self.clock,
state: self.state,
truncated: clock < self.journal_floor,
}
}
pub fn take_pending_invalidations(&mut self) -> Vec<(PathBuf, InvalidateReason)> {
std::mem::take(&mut self.pending_invalidations)
}
pub(crate) fn restore_pending_invalidations(
&mut self,
invalidations: Vec<(PathBuf, InvalidateReason)>,
) {
self.pending_invalidations.extend(invalidations);
}
pub fn lookup(&self, path: &Path) -> Option<EntryId> {
let mut current = EntryId::ROOT;
for part in normalize(path)? {
current = self.child(current, part)?;
}
Some(current)
}
pub fn rollup(&self, path: &Path) -> Option<RollUp> {
let id = self.lookup(path)?;
let entry = self.entry(id);
entry.kind.is_dir().then(|| self.named_rollup(&entry.rollup().all))
}
pub fn partition_rollup(&self, path: &Path) -> crate::Result<Option<PartitionRollUp>> {
self.require_observed_controls()?;
Ok(self
.lookup(path)
.map(|id| self.entry(id))
.filter(|entry| entry.kind.is_dir())
.map(|entry| self.named_partitions(entry.rollup())))
}
pub fn partition_rollup_summary(
&self,
path: &Path,
) -> crate::Result<Option<PartitionRollUpSummary>> {
self.require_observed_controls()?;
Ok(self
.lookup(path)
.map(|id| self.entry(id))
.filter(|entry| entry.kind.is_dir())
.map(|entry| partition_summary(entry.rollup())))
}
pub(crate) fn opened_is_ignored(&self, id: EntryId) -> bool {
debug_assert!(self.observes_controls(), "an opened root observes control state");
self.entry(id).ignored
}
pub(crate) fn entry_value(&self, path: &Path) -> Option<crate::EntryValue> {
let id = self.lookup(path)?;
Some(self.entry_value_of(id, path))
}
pub(crate) fn entry_value_of(&self, id: EntryId, path: &Path) -> crate::EntryValue {
let entry = self.entry(id);
crate::EntryValue {
path: path.to_path_buf(),
portable_path: crate::opened::read::portable_path(path),
kind: entry.kind,
attrs: entry.attrs,
ignored: entry.ignored,
classification: (entry.kind == EntryKind::File)
.then(|| self.types.classify_name(path.file_name().unwrap_or_default())),
rollup: entry.kind.is_dir().then(|| partition_summary(entry.rollup())),
children_complete: entry.kind.is_dir().then(|| entry.directory().children_complete),
}
}
pub(crate) fn portable_children(&self, path: &Path) -> Option<&PortableChildren> {
self.serving.as_ref()?.portable_children.get(path)
}
pub(crate) fn portable_entries(&self) -> &BTreeMap<crate::PortablePath, EntryId> {
&self.serving.as_ref().expect("opened-root reads require serving indexes").portable_entries
}
#[cfg(test)]
pub(crate) const fn serving_indexes_enabled(&self) -> bool {
self.serving.is_some()
}
fn insert_serving_entry(&mut self, path: &Path, kind: EntryKind, attrs: Attrs, id: EntryId) {
if path.as_os_str().is_empty() || self.serving.is_none() {
return;
}
let file = (kind == EntryKind::File).then(|| {
(
self.classify(path).file_type.as_str().to_string(),
path.file_name(),
self.entry(id).ignored,
self.entry(id).parent,
)
});
let arena = &self.arena;
let Some(serving) = self.serving.as_mut() else {
return;
};
if let Some((name, exact_name, ignored, parent)) = file {
let semantic = serving.intern_semantic(&name);
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving.semantic_by_directory.entry(directory).or_default();
merge_semantic(&mut partition.all, semantic, attrs);
if !ignored {
merge_semantic(&mut partition.unignored, semantic, attrs);
}
ancestor = retained_parent(arena, directory);
}
if let Some(exact_name) = exact_name.and_then(|name| serving.exact_name_id(name)) {
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving.exact_name_by_directory.entry(directory).or_default();
merge_semantic(&mut partition.all, exact_name, attrs);
if !ignored {
merge_semantic(&mut partition.unignored, exact_name, attrs);
}
ancestor = retained_parent(arena, directory);
}
}
}
let portable = crate::opened::read::portable_path(path);
serving.portable_entries.insert(portable.clone(), id);
if kind == EntryKind::File {
serving.recent_files.insert(RecentKey {
mtime_ns: attrs.mtime_ns,
portable_path: portable,
id,
});
}
let parent = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
let Some(name) = path.file_name().map(crate::opened::read::portable_component) else {
return;
};
let children = serving.portable_children.entry(parent).or_default();
if kind.is_dir() {
children.directories.insert(name, id);
} else {
children.nondirectories.insert(name, id);
}
}
fn remove_serving_entry(&mut self, path: &Path, kind: EntryKind, attrs: Attrs, id: EntryId) {
if path.as_os_str().is_empty() {
return;
}
let Some(serving) = self.serving.as_mut() else {
return;
};
let portable = crate::opened::read::portable_path(path);
serving.portable_entries.remove(&portable);
if kind == EntryKind::File {
serving.recent_files.remove(&RecentKey {
mtime_ns: attrs.mtime_ns,
portable_path: portable,
id,
});
}
let parent = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
let remove_parent = if let Some(children) = serving.portable_children.get_mut(&parent) {
if let Some(name) = path.file_name().map(crate::opened::read::portable_component) {
if kind.is_dir() {
children.directories.remove(&name);
} else {
children.nondirectories.remove(&name);
}
}
children.directories.is_empty() && children.nondirectories.is_empty()
} else {
false
};
if remove_parent {
serving.portable_children.remove(&parent);
}
if kind.is_dir() {
serving.portable_children.remove(path);
}
}
fn remove_serving_file_semantics(&mut self, path: &Path, id: EntryId, attrs: Attrs) {
if self.serving.is_none() || self.entry(id).kind != EntryKind::File {
return;
}
let name = self.classify(path).file_type.as_str().to_string();
let exact_name = path.file_name();
let ignored = self.entry(id).ignored;
let parent = self.entry(id).parent;
let arena = &self.arena;
let serving = self.serving.as_mut().expect("checked above");
let semantic = *serving
.semantic_ids
.get(&name)
.expect("every served file has an interned semantic type");
let mut empty = Vec::new();
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving
.semantic_by_directory
.get_mut(&directory)
.expect("every served file contributes to every ancestor");
unmerge_semantic(&mut partition.all, semantic, attrs);
if !ignored {
unmerge_semantic(&mut partition.unignored, semantic, attrs);
}
if partition.all.is_empty() && partition.unignored.is_empty() {
empty.push(directory);
}
ancestor = retained_parent(arena, directory);
}
for ancestor in empty {
serving.semantic_by_directory.remove(&ancestor);
}
serving.release_semantic(semantic, 1);
if let Some(exact_name) = exact_name.and_then(|name| serving.exact_name_id(name)) {
let mut exact_empty = Vec::new();
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving
.exact_name_by_directory
.get_mut(&directory)
.expect("every declared exact-name file contributes to every ancestor");
unmerge_semantic(&mut partition.all, exact_name, attrs);
if !ignored {
unmerge_semantic(&mut partition.unignored, exact_name, attrs);
}
if partition.all.is_empty() && partition.unignored.is_empty() {
exact_empty.push(directory);
}
ancestor = retained_parent(arena, directory);
}
for ancestor in exact_empty {
serving.exact_name_by_directory.remove(&ancestor);
}
}
}
fn remove_serving_subtree_semantics(&mut self, root: EntryId, path: &Path) {
if self.serving.is_none() {
return;
}
match self.entry(root).kind {
EntryKind::File => {
let attrs = self.entry(root).attrs;
self.remove_serving_file_semantics(path, root, attrs);
}
EntryKind::Dir => {
let parent = self.entry(root).parent;
let mut stack = vec![root];
let mut directories = Vec::new();
while let Some(id) = stack.pop() {
let entry = self.entry(id);
if !entry.kind.is_dir() {
continue;
}
directories.push(id);
stack.extend(self.child_ids(id));
}
let arena = &self.arena;
let serving = self.serving.as_mut().expect("checked above");
let contribution =
serving.semantic_by_directory.get(&root).cloned().unwrap_or_default();
let exact_contribution =
serving.exact_name_by_directory.get(&root).cloned().unwrap_or_default();
let mut empty = Vec::new();
if !contribution.all.is_empty() || !contribution.unignored.is_empty() {
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving
.semantic_by_directory
.get_mut(&directory)
.expect("a semantic subtree contributes to every ancestor");
unmerge_semantic_map(&mut partition.all, &contribution.all);
unmerge_semantic_map(&mut partition.unignored, &contribution.unignored);
if partition.all.is_empty() && partition.unignored.is_empty() {
empty.push(directory);
}
ancestor = retained_parent(arena, directory);
}
}
let mut exact_empty = Vec::new();
if !exact_contribution.all.is_empty() || !exact_contribution.unignored.is_empty() {
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving
.exact_name_by_directory
.get_mut(&directory)
.expect("an exact-name subtree contributes to every ancestor");
unmerge_semantic_map(&mut partition.all, &exact_contribution.all);
unmerge_semantic_map(
&mut partition.unignored,
&exact_contribution.unignored,
);
if partition.all.is_empty() && partition.unignored.is_empty() {
exact_empty.push(directory);
}
ancestor = retained_parent(arena, directory);
}
}
for ancestor in empty {
serving.semantic_by_directory.remove(&ancestor);
}
for ancestor in exact_empty {
serving.exact_name_by_directory.remove(&ancestor);
}
for directory in directories {
serving.semantic_by_directory.remove(&directory);
serving.exact_name_by_directory.remove(&directory);
}
for (semantic, tally) in contribution.all {
serving.release_semantic(semantic, tally.files);
}
}
EntryKind::Symlink | EntryKind::Other => {}
}
}
fn move_serving_file_partition(
&mut self,
path: &Path,
id: EntryId,
previous_ignored: bool,
current_ignored: bool,
) {
if previous_ignored == current_ignored
|| self.serving.is_none()
|| self.entry(id).kind != EntryKind::File
{
return;
}
let name = self.classify(path).file_type.as_str().to_string();
let exact_name = path.file_name();
let attrs = self.entry(id).attrs;
let parent = self.entry(id).parent;
let arena = &self.arena;
let serving = self.serving.as_mut().expect("checked above");
let semantic = *serving
.semantic_ids
.get(&name)
.expect("every served file has an interned semantic type");
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving
.semantic_by_directory
.get_mut(&directory)
.expect("every served file contributes to every ancestor");
if current_ignored {
unmerge_semantic(&mut partition.unignored, semantic, attrs);
} else {
merge_semantic(&mut partition.unignored, semantic, attrs);
}
ancestor = retained_parent(arena, directory);
}
if let Some(exact_name) = exact_name.and_then(|name| serving.exact_name_id(name)) {
let mut ancestor = parent;
while let Some(directory) = ancestor {
let partition = serving
.exact_name_by_directory
.get_mut(&directory)
.expect("every declared exact-name file contributes to every ancestor");
if current_ignored {
unmerge_semantic(&mut partition.unignored, exact_name, attrs);
} else {
merge_semantic(&mut partition.unignored, exact_name, attrs);
}
ancestor = retained_parent(arena, directory);
}
}
}
pub fn attrs(&self, path: &Path) -> Option<&Attrs> {
Some(&self.entry(self.lookup(path)?).attrs)
}
pub fn kind(&self, path: &Path) -> Option<EntryKind> {
Some(self.entry(self.lookup(path)?).kind)
}
pub fn is_ignored(&self, path: &Path) -> crate::Result<Option<bool>> {
self.require_observed_controls()?;
Ok(self.ignored_classification(path))
}
pub fn ignored_classification(&self, path: &Path) -> Option<bool> {
let id = self.lookup(path)?;
self.ignored_classification_of(path, id)
}
pub(crate) fn ignored_classification_of(&self, path: &Path, id: EntryId) -> Option<bool> {
if !self.observes_controls() || !self.control_classification_known(path) {
return None;
}
self.try_entry(id).map(|entry| entry.ignored)
}
pub(crate) fn entry_ignored(&self, id: EntryId) -> Option<bool> {
self.try_entry(id).map(|entry| entry.ignored)
}
pub(crate) fn control_classification_known(&self, path: &Path) -> bool {
path.parent().is_none_or(|directory| self.controls_known_in(directory))
}
pub(crate) fn children_classification_known(&self, directory: &Path) -> bool {
self.observes_controls() && self.controls_known_in(directory)
}
fn controls_known_in(&self, directory: &Path) -> bool {
self.controls.children_classification_known(directory)
&& (self.unreadable_control_paths.is_empty()
|| !directory.ancestors().any(|ancestor| {
self.unreadable_control_paths
.iter()
.any(|control| control.parent() == Some(ancestor))
}))
}
pub fn ignored_classification_complete_below(&self, path: &Path) -> bool {
self.observes_controls()
&& self.controls.classification_known(path)
&& !self.unreadable_control_paths.iter().any(|control| {
control.starts_with(path)
|| path
.parent()
.into_iter()
.flat_map(Path::ancestors)
.any(|directory| control.parent() == Some(directory))
})
&& !self.controls.refusals().any(|refusal| {
refusal.path.starts_with(path)
|| path
.parent()
.into_iter()
.flat_map(Path::ancestors)
.any(|directory| refusal.path.parent() == Some(directory))
})
}
pub fn children(
&self,
path: &Path,
) -> Option<impl DoubleEndedIterator<Item = (&OsStr, EntryId)> + ExactSizeIterator + '_> {
let id = self.lookup(path)?;
let entry = self.entry(id);
entry.kind.is_dir().then(|| IndexChildren::new(self, entry))
}
pub fn children_of(
&self,
id: EntryId,
) -> Option<impl DoubleEndedIterator<Item = (&OsStr, EntryId)> + ExactSizeIterator + '_> {
Some(IndexChildren::new(self, self.try_entry(id)?))
}
pub fn path_of(&self, id: EntryId) -> Option<PathBuf> {
let mut parts = Vec::new();
let mut current = Some(id);
while let Some(node) = current {
let entry = self.try_entry(node)?;
if entry.parent.is_some() {
parts.push(entry.name.as_os_str());
}
current = entry.parent;
}
parts.reverse();
Some(parts.iter().collect())
}
pub fn rollup_of(&self, id: EntryId) -> Option<RollUp> {
let entry = self.try_entry(id)?;
entry.kind.is_dir().then(|| self.named_rollup(&entry.rollup().all))
}
pub(crate) fn partition_scalars_of(
&self,
id: EntryId,
) -> Option<(RollUpScalars, RollUpScalars)> {
let entry = self.try_entry(id)?;
entry.kind.is_dir().then(|| {
let rollup = entry.rollup();
(RollUpScalars::from(&rollup.all), RollUpScalars::from(&rollup.unignored))
})
}
pub(crate) const fn is_folded(&self) -> bool {
self.folded.is_some()
}
pub(crate) fn folded_children(&self, id: EntryId) -> Option<FoldedFiles> {
let folded = self.folded.as_ref()?;
self.try_entry(id)?;
folded.get(id.idx()).copied().filter(|folded| folded.files > 0)
}
pub(crate) fn directory_complete_of(&self, id: EntryId) -> Option<bool> {
let entry = self.try_entry(id)?;
(entry.kind == EntryKind::Dir).then(|| entry.directory().children_complete)
}
pub fn attrs_of(&self, id: EntryId) -> Option<&Attrs> {
Some(&self.try_entry(id)?.attrs)
}
pub fn kind_of(&self, id: EntryId) -> Option<EntryKind> {
Some(self.try_entry(id)?.kind)
}
pub fn name_of(&self, id: EntryId) -> Option<&OsStr> {
Some(&self.try_entry(id)?.name)
}
pub fn content(&self) -> Option<&ContentIndex> {
self.content.as_deref()
}
pub fn content_rollup(&self, path: &Path) -> Option<&ContentRollUp> {
self.content()?.rollup(path)
}
pub fn content_set(&self) -> AnalysisSet {
self.content().and_then(ContentIndex::profile).unwrap_or(AnalysisSet::NONE)
}
pub(crate) fn content_has_pending(&self, profile: AnalysisSet) -> bool {
if !profile.is_enabled() {
return false;
}
let wanted = self.content_identity(profile);
let Some(content) = self.content().and_then(|content| content.admit(&wanted)) else {
return true;
};
if self.scope.population == crate::query::IgnoredEntries::Include {
return u64::try_from(content.len()).unwrap_or(u64::MAX)
< self.entry(EntryId::ROOT).rollup().files;
}
let mut pending = false;
self.for_each_analysis_file(profile, |_, _, attrs, path| {
pending |=
content.file(&path).is_none_or(|record| record.fingerprint != attrs.fingerprint());
});
pending
}
pub fn content_identity(&self, analysis: AnalysisSet) -> crate::ContentTierIdentity {
crate::ContentTierIdentity::for_request(
crate::EntryTierIdentity::of_scope(self.scope),
analysis,
)
}
pub(crate) fn prepare_content_analysis(&mut self, request: crate::content::AnalysisRequest) {
if !request.profile.is_enabled() {
return;
}
let identity = self.content_identity(request.profile);
self.content.get_or_insert_with(|| Box::new(ContentIndex::default())).prepare(identity);
}
pub(crate) fn set_content_tier_state(
&mut self,
source: Source,
freshness: Freshness,
observed_at_ns: Option<i64>,
) {
if let Some(content) = self.content.as_deref_mut() {
content.set_state(crate::content::ContentTierState {
source,
freshness,
observed_at_ns,
});
}
}
#[cfg(test)]
pub(crate) fn analysis_candidates(&self, profile: AnalysisSet) -> Vec<AnalysisCandidate> {
let root_files = self.entry(EntryId::ROOT).rollup().files;
let mut candidates = Vec::with_capacity(usize::try_from(root_files).unwrap_or(0));
self.walk_analysis_files(
profile,
&mut AnalysisWalk::start(),
|id, revision, attrs, relative_path| {
candidates.push(self.analysis_candidate(id, revision, attrs, relative_path));
true
},
);
candidates
}
fn analysis_candidate(
&self,
entry_id: EntryId,
revision: u64,
attrs: Attrs,
relative_path: PathBuf,
) -> AnalysisCandidate {
AnalysisCandidate {
entry_id,
revision,
absolute_path: self.root_path.join(&relative_path),
classification: self.classify(&relative_path),
relative_path,
attrs,
}
}
pub(crate) fn restore_analysis_candidates(
&self,
profile: AnalysisSet,
) -> (HashMap<PathBuf, RestoreCandidate>, u64) {
let root_files = self.entry(EntryId::ROOT).rollup().files;
let mut candidates = HashMap::with_capacity(usize::try_from(root_files).unwrap_or(0));
let mut visited = 0_u64;
self.for_each_analysis_file(profile, |id, revision, attrs, relative_path| {
visited = visited.saturating_add(1);
candidates.insert(
relative_path.clone(),
RestoreCandidate { entry_id: id, revision, relative_path, attrs },
);
});
(candidates, visited)
}
fn for_each_analysis_file(
&self,
profile: AnalysisSet,
mut visit: impl FnMut(EntryId, u64, Attrs, PathBuf),
) {
self.walk_analysis_files(
profile,
&mut AnalysisWalk::start(),
|id, revision, attrs, path| {
visit(id, revision, attrs, path);
true
},
);
}
fn walk_analysis_files(
&self,
profile: AnalysisSet,
walk: &mut AnalysisWalk,
mut visit: impl FnMut(EntryId, u64, Attrs, PathBuf) -> bool,
) {
if !profile.is_enabled() {
return;
}
loop {
let (parent, parent_path, resume) = match walk.listing.take() {
Some((parent, parent_path, resume)) => (parent, parent_path, Some(resume)),
None => match walk.pending.pop() {
Some((parent, parent_path)) => (parent, parent_path, None),
None => return,
},
};
let visited = resume.as_ref().map_or(0, |resume| resume.position);
let children = self.resumed_children(parent, resume.as_ref());
#[cfg(test)]
let children = children.inspect(|_| {
ANALYSIS_CHILD_STEPS.with(|steps| steps.set(steps.get() + 1));
});
for (position, (name, id)) in (visited + 1..).zip(children) {
let entry = self.entry(id);
if entry.kind == EntryKind::Dir {
walk.pending.push((id, parent_path.join(name)));
continue;
}
if entry.kind != EntryKind::File {
continue;
}
let relative_path = parent_path.join(name);
if !self.scope.population.admits(entry.ignored)
|| (self.scope.population != crate::query::IgnoredEntries::Include
&& !self.control_classification_known(&relative_path))
{
continue;
}
if !visit(id, entry.revision, entry.attrs, relative_path) {
let resume = ListingResume { position, last: name.to_os_string() };
walk.listing = Some((parent, parent_path, resume));
return;
}
}
}
}
fn resumed_children(
&self,
parent: EntryId,
resume: Option<&ListingResume>,
) -> ResumedChildren<'_> {
use std::ops::Bound::{Excluded, Unbounded};
let Some(entry) = self.try_entry(parent) else { return ResumedChildren::Empty };
match entry.directory.as_deref().map(|directory| &directory.children) {
None => ResumedChildren::Empty,
Some(DirectoryChildren::Sorted(ids)) => {
let from = resume.map_or(0, |resume| resume.position).min(ids.len());
ResumedChildren::Sorted { index: self, ids: ids[from..].iter() }
}
Some(DirectoryChildren::Mutable(children)) => ResumedChildren::Mutable(match resume {
Some(resume) => {
children.range::<OsStr, _>((Excluded(resume.last.as_os_str()), Unbounded))
}
None => children.range::<OsStr, _>(..),
}),
}
}
#[cfg(test)]
pub(crate) fn pending_analysis_candidates(
&self,
request: crate::content::AnalysisRequest,
) -> Vec<AnalysisCandidate> {
self.next_analysis_candidates(request, &mut AnalysisWalk::start(), usize::MAX)
}
fn pending_analysis(
held: Option<crate::stored_state::ContentProjection<'_>>,
relative_path: &Path,
attrs: &Attrs,
) -> bool {
held.and_then(|content| content.file(relative_path))
.is_none_or(|record| record.fingerprint != attrs.fingerprint() || !record.is_reusable())
}
pub(crate) fn next_analysis_candidates(
&self,
request: crate::content::AnalysisRequest,
walk: &mut AnalysisWalk,
limit: usize,
) -> Vec<AnalysisCandidate> {
let wanted = self.content_identity(request.profile);
let held = self.content().and_then(|content| content.admit(&wanted));
let root_files = usize::try_from(self.entry(EntryId::ROOT).rollup().files).unwrap_or(0);
let mut candidates = Vec::with_capacity(limit.min(root_files));
self.walk_analysis_files(request.profile, walk, |id, revision, attrs, relative_path| {
if Self::pending_analysis(held, &relative_path, &attrs) {
candidates.push(self.analysis_candidate(id, revision, attrs, relative_path));
}
candidates.len() < limit
});
candidates
}
pub(crate) fn count_pending_analysis_candidates(
&self,
request: crate::content::AnalysisRequest,
) -> u64 {
let wanted = self.content_identity(request.profile);
let held = self.content().and_then(|content| content.admit(&wanted));
let mut count = 0_u64;
self.walk_analysis_files(
request.profile,
&mut AnalysisWalk::start(),
|_, _, attrs, path| {
count += u64::from(Self::pending_analysis(held, &path, &attrs));
true
},
);
count
}
pub(crate) fn apply_analysis(
&mut self,
observation: AnalysisObservation,
) -> AnalysisApplyOutcome {
self.apply_analysis_record(observation)
}
pub(crate) fn apply_restored_analysis(
&mut self,
candidate: RestoreCandidate,
analysis: crate::stored_state::AdmittedRecord<'_>,
) -> AnalysisApplyOutcome {
let Some(entry) = self.try_entry(candidate.entry_id) else {
return AnalysisApplyOutcome::Stale;
};
if entry.kind != EntryKind::File
|| entry.revision != candidate.revision
|| entry.attrs.fingerprint() != candidate.attrs.fingerprint()
{
return AnalysisApplyOutcome::Stale;
}
let Some(content) = self.content.as_mut() else {
return AnalysisApplyOutcome::Stale;
};
if content.commit_without_rollup(candidate.relative_path, analysis) {
AnalysisApplyOutcome::Applied
} else {
AnalysisApplyOutcome::Stale
}
}
pub(crate) fn rebuild_content_rollups(&mut self) {
if let Some(content) = self.content.as_mut() {
content.rebuild_rollups();
}
}
fn apply_analysis_record(&mut self, observation: AnalysisObservation) -> AnalysisApplyOutcome {
let candidate = &observation.candidate;
let Some(entry) = self.try_entry(candidate.entry_id) else {
return AnalysisApplyOutcome::Stale;
};
if entry.kind != EntryKind::File
|| entry.revision != candidate.revision
|| entry.attrs.fingerprint() != candidate.attrs.fingerprint()
|| self.classify(&candidate.relative_path) != candidate.classification
{
return AnalysisApplyOutcome::Stale;
}
let Some(content) = self.content.as_mut() else {
return AnalysisApplyOutcome::Stale;
};
if content.commit(
candidate.relative_path.clone(),
observation.profile,
&observation.provenance,
observation.analysis,
) {
AnalysisApplyOutcome::Applied
} else {
AnalysisApplyOutcome::Stale
}
}
pub fn clear_content(&mut self) {
self.content = None;
}
fn try_entry(&self, id: EntryId) -> Option<&Entry> {
match self.arena.get(id.idx())? {
Slot::Occupied { generation, entry } if *generation == id.generation => Some(entry),
Slot::Occupied { .. } | Slot::Free { .. } => None,
}
}
fn expectation_matches(&self, op: &Op, expected: PathExpectation) -> bool {
let current = self.path_state(op.path());
if self.holds_target(op, current) {
return true;
}
if current != expected.state {
return false;
}
let require_structure = match (op, expected.state) {
(Op::Remove { .. }, _) => true,
(Op::Upsert { kind, .. }, PathState::Present { kind: baseline, .. }) => {
*kind != baseline
}
(
Op::Upsert { .. }
| Op::ControlUpsert { .. }
| Op::ControlRemove { .. }
| Op::InvalidateSubtree { .. },
_,
) => false,
};
if !same_target(self.entry_identity(op.path()), expected.entry(), require_structure) {
return false;
}
match expected.absence_guard() {
Some(expected) => self
.absence_guard_identity(op.path())
.is_some_and(|current| current.same_absence_guard(expected)),
None => true,
}
}
fn holds_target(&self, op: &Op, current: PathState) -> bool {
match op {
Op::Upsert { kind, attrs, .. } => {
current == PathState::Present { kind: *kind, attrs: *attrs }
}
Op::Remove { .. } => current == PathState::Absent,
Op::ControlUpsert { path, source } => self.controls.source_is(path, source),
Op::ControlRemove { path } => !self.controls.contains(path),
Op::InvalidateSubtree { .. } => false,
}
}
fn absence_guard_identity(&self, path: &Path) -> Option<EntryIdentity> {
let parts = normalize(path)?;
let (_, ancestors) = parts.split_last()?;
let mut current = EntryId::ROOT;
for part in ancestors {
let Some(child) = self.child(current, part) else {
break;
};
current = child;
}
Some(self.identity(current))
}
fn validate_known_ancestry(
&self,
ops: &[ObservationOp],
accepted: &[bool],
) -> crate::Result<()> {
if let Some((path, reconcile_from)) =
self.unknown_ancestry(ops, accepted).into_iter().next()
{
return Err(crate::Error::UnknownAncestry { path, reconcile_from });
}
Ok(())
}
fn accepted_operations(&self, ops: &[ObservationOp]) -> Vec<bool> {
ops.iter()
.map(|observed| match observed.expectation {
Expectation::Any => true,
Expectation::State(expected) => self.expectation_matches(&observed.op, expected),
})
.collect()
}
fn prepare_scanner_batch(
&self,
batch: crate::scan::ScannerBatch,
) -> crate::Result<PreparedObservation> {
let ops = batch.into_ops();
let mut parents = Vec::with_capacity(ops.len());
let mut last_parent: Option<(&Path, ResolvedParent)> = None;
let mut has_batch_parents = false;
let mut path_comparisons = 0_u64;
let mut replaces_kind = false;
for (op_index, observed) in ops.iter().enumerate() {
if !matches!(observed.expectation, Expectation::Any) {
return Err(crate::Error::UnsupportedScanConfig(
"scanner batches contain unconditional discoveries only",
));
}
let op = &observed.op;
let path = op.path();
if path.as_os_str().is_empty() {
return Err(crate::Error::UnsupportedScanConfig(
"scanner batches cannot mutate the index root",
));
}
for component in path.components() {
match component {
Component::Normal(_) => {}
Component::CurDir => {
return Err(crate::Error::UnsupportedScanConfig(
"scanner batches require canonical relative paths",
));
}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(crate::Error::PathEscapesRoot(path.to_path_buf()));
}
}
}
match op {
Op::Upsert { .. } => {}
Op::ControlUpsert { .. } | Op::ControlRemove { .. }
if crate::control::is_control_file(path) => {}
Op::ControlUpsert { .. } | Op::ControlRemove { .. } => {
return Err(crate::Error::InvalidControlPath(path.to_path_buf()));
}
Op::Remove { .. } | Op::InvalidateSubtree { .. } => {
return Err(crate::Error::UnsupportedScanConfig(
"scanner batches contain discoveries only",
));
}
}
if replaces_kind {
continue;
}
let parent_path = path.parent().expect("a non-root relative path has a parent");
if last_parent.is_some() {
path_comparisons = path_comparisons.saturating_add(1);
}
let same_parent = last_parent
.filter(|(previous, _)| *previous == parent_path)
.map(|(_, parent)| parent);
let parent = if let Some(parent) = same_parent {
parent
} else {
self.lookup(parent_path)
.filter(|id| self.entry(*id).kind.is_dir())
.map(ResolvedParent::Existing)
.or_else(|| Self::earlier_scanner_parent(&ops, op_index, parent_path))
.ok_or_else(|| crate::Error::UnknownAncestry {
path: path.to_path_buf(),
reconcile_from: PathBuf::new(),
})?
};
if let (Op::Upsert { kind, .. }, ResolvedParent::Existing(parent)) = (op, parent) {
if path.file_name().is_some_and(|name| {
self.child(parent, name).is_some_and(|child| self.entry(child).kind != *kind)
}) {
replaces_kind = true;
continue;
}
}
has_batch_parents |= matches!(parent, ResolvedParent::Earlier(_));
parents.push(parent);
last_parent = Some((parent_path, parent));
}
if replaces_kind {
return prepare_observation(&Observation::from_ops(ops));
}
crate::counters::bump(|counts| {
counts.ancestry_path_comparisons =
counts.ancestry_path_comparisons.saturating_add(path_comparisons);
counts.ancestry_parent_proofs = counts
.ancestry_parent_proofs
.saturating_add(u64::try_from(ops.len()).unwrap_or(u64::MAX));
});
Ok(PreparedObservation {
ops,
ancestry: PreparedAncestry::Scanner { parents, has_batch_parents },
#[cfg(test)]
reject_before_apply: false,
})
}
fn earlier_scanner_parent(
ops: &[ObservationOp],
before: usize,
parent_path: &Path,
) -> Option<ResolvedParent> {
let (op_index, op) = ops[..before].iter().enumerate().rev().find(
|(_, observed)| matches!(&observed.op, Op::Upsert { path, .. } if path == parent_path),
)?;
let Op::Upsert { kind, .. } = &op.op else {
unreachable!("the search selected an upsert");
};
kind.is_dir().then_some(ResolvedParent::Earlier(op_index))
}
fn projected_controls(
&self,
ops: &[ObservationOp],
accepted: &[bool],
) -> crate::Result<Option<crate::control::ControlTable>> {
self.projected_controls_from(
ops.iter()
.zip(accepted)
.filter_map(|(observed, accepted)| accepted.then_some(&observed.op)),
)
}
fn projected_controls_from<'a>(
&self,
ops: impl Iterator<Item = &'a Op> + Clone,
) -> crate::Result<Option<crate::control::ControlTable>> {
if self.controls_unchanged_by(ops.clone()) {
return Ok(None);
}
let mut projected = self.controls.clone();
#[cfg(test)]
CONTROL_PROJECTION_CLONES.with(|clones| clones.set(clones.get() + 1));
let mut structure = StructuralOverlay::default();
for op in ops {
match op {
Op::Upsert { path, kind, .. } => {
if crate::control::is_control_file(path) && *kind != EntryKind::File {
projected.remove(path)?;
}
if structure.kind(self, path) == Some(EntryKind::Dir) && !kind.is_dir() {
projected.remove_subtree(path);
}
structure.upsert(self, path, *kind);
}
Op::Remove { path } => {
if crate::control::is_control_file(path) {
projected.remove(path)?;
}
projected.remove_subtree(path);
structure.remove(self, path);
}
Op::ControlUpsert { path, source } => {
projected.upsert(path, source.clone())?;
}
Op::ControlRemove { path } => {
projected.remove(path)?;
}
Op::InvalidateSubtree { .. } => {}
}
}
Ok(Some(projected))
}
fn controls_unchanged_by<'a>(&self, ops: impl Iterator<Item = &'a Op>) -> bool {
if self.controls.is_vacant() {
return ops.into_iter().all(|op| match op {
Op::ControlUpsert { .. } => false,
Op::ControlRemove { path } => self.controls.remove_is_inert(path),
Op::Upsert { .. } | Op::Remove { .. } | Op::InvalidateSubtree { .. } => true,
});
}
ops.into_iter().all(|op| match op {
Op::ControlUpsert { path, source } => self.controls.upsert_is_inert(path, source),
Op::ControlRemove { path } => self.controls.remove_is_inert(path),
Op::Upsert { path, kind, .. } => {
let drops_control = *kind != EntryKind::File
&& crate::control::is_control_file(path)
&& self.controls.contains(path);
let prunes_subtree = !kind.is_dir() && self.controls.has_record_at_or_below(path);
!drops_control && !prunes_subtree
}
Op::Remove { path } => {
let drops_control =
crate::control::is_control_file(path) && self.controls.contains(path);
!drops_control && !self.controls.has_record_at_or_below(path)
}
Op::InvalidateSubtree { .. } => true,
})
}
fn apply_control_transition<C: ConsequenceSink>(
&mut self,
projected: Option<crate::control::ControlTable>,
stats: &mut ApplyStats,
effects: &mut C,
) {
let Some(projected) = projected else {
return;
};
let changes = projected.changes_from(&self.controls);
let refusals = projected.refusal_changes_from(&self.controls);
if changes.is_empty() && refusals.is_empty() {
return;
}
let affected: Vec<PathBuf> = changes
.iter()
.filter_map(|(path, _, _)| crate::control::ControlTable::affected_subtree(path).ok())
.collect();
self.controls = projected;
stats.controls = u64::try_from(changes.len() + refusals.len()).unwrap_or(u64::MAX);
for (path, previous, current) in changes {
effects.change(|| EffectiveChange::ControlUpdated { path, previous, current });
}
for (path, previous, current) in refusals {
effects.change(|| EffectiveChange::ControlRefusalUpdated { path, previous, current });
}
self.reclassify_controlled_subtrees(&affected, stats, effects);
}
fn reclassify_controlled_subtrees<C: ConsequenceSink>(
&mut self,
affected: &[PathBuf],
stats: &mut ApplyStats,
effects: &mut C,
) {
let mut roots: Vec<PathBuf> = affected.to_vec();
roots.sort();
roots.dedup();
let mut collapsed = Vec::new();
for root in roots {
if collapsed.iter().any(|ancestor: &PathBuf| root.starts_with(ancestor)) {
continue;
}
collapsed.push(root);
}
let mut moved = false;
for root in collapsed {
let Some(root_id) = self.lookup(&root) else {
continue;
};
let children: Vec<(PathBuf, EntryId)> = self
.children_of(root_id)
.expect("controlled subtree root is live")
.map(|(name, id)| (root.join(name), id))
.collect();
let mut queue = VecDeque::from(children);
while let Some((path, id)) = queue.pop_front() {
#[cfg(test)]
RECLASSIFY_VISITS.with(|visits| visits.set(visits.get() + 1));
let entry = self.entry(id);
let parent_ignored = entry.parent.is_some_and(|parent| self.entry(parent).ignored);
let current = entry.ignored;
let next = parent_ignored
|| self.controls.matcher_for(&path).is_ignored(entry.kind.is_dir());
let descendants: Vec<(PathBuf, EntryId)> = self
.children_of(id)
.expect("controlled subtree entry is live")
.map(|(name, child)| (path.join(name), child))
.collect();
if current != next {
self.move_serving_file_partition(&path, id, current, next);
self.entry_mut(id).ignored = next;
stats.reclassified += 1;
effects.change(|| EffectiveChange::Reclassified {
path: path.clone(),
previous_ignored: current,
current_ignored: next,
});
moved = true;
}
queue.extend(descendants);
}
}
if moved {
self.rebuild_unignored_rollups();
}
}
fn rebuild_unignored_rollups(&mut self) {
let mut order = Vec::with_capacity(usize::try_from(self.live).unwrap_or(0));
let mut stack = vec![EntryId::ROOT];
while let Some(id) = stack.pop() {
order.push(id);
if self.entry(id).kind.is_dir() {
stack.extend(self.child_ids(id));
}
if self.entry(id).kind.is_dir() {
self.entry_mut(id).rollup_mut().unignored = InternedRollUp::default();
}
}
for id in order.into_iter().rev() {
let Some(parent) = self.entry(id).parent else {
continue;
};
let contribution = self.contribution(id).unignored;
self.entry_mut(parent).rollup_mut().unignored.merge(&contribution);
}
}
fn unknown_ancestry(
&self,
ops: &[ObservationOp],
accepted: &[bool],
) -> Vec<(PathBuf, PathBuf)> {
let mut structure = StructuralOverlay::default();
let mut unknown = Vec::new();
let mut overlay_inserts = 0_u64;
let mut path_comparisons = 0_u64;
let mut parent_proofs = 0_u64;
let count_preflight = crate::counters::enabled();
let mut proven_dir: Option<PathBuf> = None;
let mut ancestor = PathBuf::new();
for (observed, accepted) in ops.iter().zip(accepted) {
if !accepted {
continue;
}
match &observed.op {
Op::Upsert { path, .. } | Op::ControlUpsert { path, .. }
if !path.as_os_str().is_empty() =>
{
if count_preflight && proven_dir.is_some() {
path_comparisons = path_comparisons.saturating_add(1);
}
let same_proven_parent = matches!(
(path.parent(), proven_dir.as_deref()),
(Some(parent), Some(proven)) if parent == proven
);
if same_proven_parent {
if count_preflight {
parent_proofs = parent_proofs.saturating_add(1);
}
} else {
let mut reconcile_from = PathBuf::new();
let mut ancestry_known = true;
let parts = normalize(path).expect("prepared paths are canonical");
let (_, ancestors) = parts.split_last().expect("non-root path has a name");
ancestor.clear();
for part in ancestors {
ancestor.push(part);
if structure.kind(self, &ancestor) != Some(EntryKind::Dir) {
unknown.push((path.clone(), reconcile_from));
ancestry_known = false;
break;
}
reconcile_from.clone_from(&ancestor);
}
if !ancestry_known {
proven_dir = None;
continue;
}
if count_preflight {
parent_proofs = parent_proofs.saturating_add(1);
}
match &mut proven_dir {
Some(proven) => {
proven.clear();
path.parent().unwrap_or(Path::new("")).clone_into(proven);
}
None => {
proven_dir =
Some(path.parent().unwrap_or(Path::new("")).to_path_buf());
}
}
}
if let Op::Upsert { kind, .. } = &observed.op {
structure.upsert(self, path, *kind);
if count_preflight {
overlay_inserts = overlay_inserts.saturating_add(1);
}
if !kind.is_dir() {
if proven_dir.as_deref().is_some_and(|proven| proven.starts_with(path))
{
proven_dir = None;
}
}
}
}
Op::Remove { path } if !path.as_os_str().is_empty() => {
structure.remove(self, path);
if proven_dir.as_deref().is_some_and(|proven| proven.starts_with(path)) {
proven_dir = None;
}
}
Op::Upsert { .. }
| Op::Remove { .. }
| Op::ControlUpsert { .. }
| Op::ControlRemove { .. }
| Op::InvalidateSubtree { .. } => {}
}
}
if count_preflight {
crate::counters::bump(|counts| {
counts.ancestry_overlay_inserts =
counts.ancestry_overlay_inserts.saturating_add(overlay_inserts);
counts.ancestry_path_comparisons =
counts.ancestry_path_comparisons.saturating_add(path_comparisons);
counts.ancestry_parent_proofs =
counts.ancestry_parent_proofs.saturating_add(parent_proofs);
});
}
unknown
}
fn entry_identity(&self, path: &Path) -> Option<EntryIdentity> {
Some(self.identity(self.lookup(path)?))
}
fn identity(&self, id: EntryId) -> EntryIdentity {
let entry = self.entry(id);
EntryIdentity::new(
id.slot,
id.generation,
entry.revision,
entry.directory.as_deref().map_or(0, |directory| directory.children_revision),
entry.kind.is_dir(),
)
}
fn bump_revision(entry: &mut Entry) {
entry.revision = entry.revision.checked_add(1).expect("entry revision exhausted");
}
fn bump_children_revision(entry: &mut Entry) {
let directory = entry.directory_mut();
directory.children_revision =
directory.children_revision.checked_add(1).expect("entry children revision exhausted");
}
fn child(&self, parent: EntryId, name: &OsStr) -> Option<EntryId> {
let children = &self.entry(parent).directory.as_deref()?.children;
match children {
DirectoryChildren::Sorted(ids) => ids
.binary_search_by(|id| self.entry(*id).name.as_os_str().cmp(name))
.ok()
.map(|position| ids[position]),
DirectoryChildren::Mutable(children) => children.get(name).copied(),
}
}
fn child_ids(&self, parent: EntryId) -> ChildIds<'_> {
self.entry(parent).directory().children.ids()
}
fn promote_children(&mut self, parent: EntryId) {
let ids = match &mut self.entry_mut(parent).directory_mut().children {
DirectoryChildren::Sorted(ids) => std::mem::take(ids),
DirectoryChildren::Mutable(_) => return,
};
let expected = ids.len();
let children =
ids.into_iter().map(|id| (self.entry(id).name.clone(), id)).collect::<BTreeMap<_, _>>();
assert_eq!(
children.len(),
expected,
"compact child names must remain unique before promotion"
);
self.entry_mut(parent).directory_mut().children = DirectoryChildren::Mutable(children);
}
fn insert_child(&mut self, parent: EntryId, name: OsString, child: EntryId) {
self.promote_children(parent);
let entry = self.entry_mut(parent);
let DirectoryChildren::Mutable(children) = &mut entry.directory_mut().children else {
unreachable!("child promotion produces mutable storage")
};
children.insert(name, child);
Self::bump_children_revision(entry);
}
fn reserve_detached_children(&mut self, parent: EntryId, additional: usize) {
let entry = self.entry_mut(parent);
let DirectoryChildren::Sorted(children) = &mut entry.directory_mut().children else {
unreachable!("detached directories retain sorted child storage")
};
children.reserve(additional);
}
fn push_detached_child(&mut self, parent: EntryId, child: EntryId) {
let entry = self.entry_mut(parent);
let DirectoryChildren::Sorted(children) = &mut entry.directory_mut().children else {
unreachable!("detached directories retain sorted child storage")
};
children.push(child);
Self::bump_children_revision(entry);
}
fn sort_detached_children(&mut self, parent: EntryId) {
let DirectoryChildren::Sorted(ids) = &mut self.entry_mut(parent).directory_mut().children
else {
unreachable!("detached directories retain sorted child storage")
};
let mut ids = std::mem::take(ids);
ids.sort_unstable_by(|left, right| self.entry(*left).name.cmp(&self.entry(*right).name));
self.entry_mut(parent).directory_mut().children = DirectoryChildren::Sorted(ids);
}
fn merge_detached_descendants(&mut self, parent: EntryId, child: EntryId) {
debug_assert!(parent.idx() < child.idx(), "cold parents must precede descendants");
let (parents, children) = self.arena.split_at_mut(child.idx());
let child_rollup = match &children[0] {
Slot::Occupied { generation, entry } if *generation == child.generation => {
entry.rollup()
}
Slot::Occupied { .. } | Slot::Free { .. } => {
panic!("detached child handle must be live: {child:?}")
}
};
let parent_entry = match &mut parents[parent.idx()] {
Slot::Occupied { generation, entry } if *generation == parent.generation => entry,
Slot::Occupied { .. } | Slot::Free { .. } => {
panic!("detached parent handle must be live: {parent:?}")
}
};
parent_entry.rollup_mut().merge(child_rollup);
}
fn remove_child(&mut self, parent: EntryId, name: &OsStr) {
self.promote_children(parent);
let entry = self.entry_mut(parent);
let DirectoryChildren::Mutable(children) = &mut entry.directory_mut().children else {
unreachable!("child promotion produces mutable storage")
};
if children.remove(name).is_some() {
Self::bump_children_revision(entry);
}
}
fn entry(&self, id: EntryId) -> &Entry {
self.try_entry(id).expect("internal entry handle must be live")
}
fn entry_mut(&mut self, id: EntryId) -> &mut Entry {
match self.arena.get_mut(id.idx()) {
Some(Slot::Occupied { generation, entry }) if *generation == id.generation => entry,
Some(Slot::Occupied { .. } | Slot::Free { .. }) | None => {
panic!("internal entry handle must be live: {id:?}")
}
}
}
fn alloc(&mut self, entry: Entry) -> EntryId {
crate::counters::bump(|c| c.entries_allocated += 1);
self.live += 1;
if let Some(free_slot) = self.free_head {
let free_idx = free_slot as usize;
let (generation, next) = match &self.arena[free_idx] {
Slot::Free { generation, next_free } => (*generation, *next_free),
Slot::Occupied { .. } => unreachable!("free list pointed at a live slot"),
};
self.free_head = next;
self.arena[free_idx] = Slot::Occupied { generation, entry };
return EntryId { slot: free_slot, generation };
}
let slot = u32::try_from(self.arena.len()).expect("index arena exceeded u32 capacity");
let id = EntryId { slot, generation: 0 };
self.arena.push(Slot::Occupied { generation: 0, entry });
id
}
fn free(&mut self, id: EntryId) {
let next_generation = match &self.arena[id.idx()] {
Slot::Occupied { generation, .. } if *generation == id.generation => {
generation.checked_add(1).expect("entry generation exhausted")
}
Slot::Occupied { .. } | Slot::Free { .. } => {
panic!("internal entry handle must be live: {id:?}")
}
};
self.arena[id.idx()] =
Slot::Free { generation: next_generation, next_free: self.free_head };
self.free_head = Some(id.slot);
self.live -= 1;
}
fn now_unix_nanos() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.and_then(|since| i64::try_from(since.as_nanos()).ok())
.unwrap_or(0)
}
pub fn provenance(&self, path: &Path) -> Option<Provenance> {
let id = self.lookup(path)?;
Some(self.provenance_of(id))
}
fn provenance_of(&self, id: EntryId) -> Provenance {
let entry = self.entry(id);
let status = self.status_of(id);
if entry.source >= Source::Revalidated {
if let Some(path) = self.path_of(id) {
if self.freshness_at(&path) == Freshness::Fresh {
if let Some(verified_at) = self.verified_at(&path) {
return Provenance {
source: Source::Revalidated,
observed_at_ns: verified_at,
status,
};
}
}
}
}
Provenance { source: entry.source, observed_at_ns: self.observed_at(entry.source), status }
}
fn status_of(&self, id: EntryId) -> Status {
let Some(path) = self.path_of(id) else {
return Status::Complete;
};
match self.freshness_at(&path) {
Freshness::Fresh | Freshness::Reconciling | Freshness::Stale => Status::Complete,
Freshness::Partial => Status::Partial,
}
}
const fn observed_at(&self, source: Source) -> i64 {
match source {
Source::Cached | Source::JournalScoped => self.captured_at_ns,
Source::Scanned | Source::Revalidated => self.scanned_at_ns,
}
}
pub(crate) const fn writing_pass_started_at_ns(&self) -> i64 {
self.writing_pass_started_at_ns
}
pub(crate) fn set_writing_pass_started_at_ns(&mut self, writing_pass_started_at_ns: i64) {
self.writing_pass_started_at_ns = writing_pass_started_at_ns;
}
pub(crate) const fn persistence_owed(&self) -> bool {
self.persistence_owed
}
pub(crate) fn set_persistence_owed(&mut self, owed: bool) {
self.persistence_owed = owed;
}
pub(crate) fn set_applying_source(&mut self, source: Source, captured_at_ns: i64) -> Source {
let previous = self.applying_source;
self.applying_source = source;
if source == Source::Cached {
self.state.source = Source::Cached;
}
if captured_at_ns != 0 {
self.captured_at_ns = captured_at_ns;
}
previous
}
fn intern_ext(&mut self, name: &str) -> ExtId {
if let Some(&id) = self.ext_ids.get(name) {
let refcount =
self.ext_refcounts.get_mut(id as usize).expect("a live id has a refcount");
*refcount = refcount.checked_add(1).expect("extension refcount exhausted");
return id;
}
let id = if let Some(id) = self.free_ext_ids.pop() {
let slot = id as usize;
self.ext_names[slot] = Some(name.to_string());
self.ext_refcounts[slot] = 1;
id
} else {
let id = ExtId::try_from(self.ext_names.len()).expect("extension interner exhausted");
self.ext_names.push(Some(name.to_string()));
self.ext_refcounts.push(1);
id
};
self.ext_ids.insert(name.to_string(), id);
id
}
fn release_ext(&mut self, id: ExtId) {
let slot = id as usize;
let refcount = self.ext_refcounts.get_mut(slot).expect("a live id has a refcount");
debug_assert!(*refcount > 0, "extension reference released twice");
*refcount -= 1;
if *refcount != 0 {
return;
}
let name = self.ext_names[slot].take().expect("a live id has a name");
let removed = self.ext_ids.remove(&name);
debug_assert_eq!(removed, Some(id), "the interner's two maps disagreed");
self.free_ext_ids.push(id);
}
fn named_rollup(&self, rollup: &InternedRollUp) -> RollUp {
let by_ext = rollup
.by_ext
.iter()
.map(|(id, tally)| {
let name = self
.ext_names
.get(*id as usize)
.and_then(Option::as_ref)
.expect("a live roll-up's extension id has a name");
(name.clone(), *tally)
})
.collect();
RollUp {
files: rollup.files,
dirs: rollup.dirs,
bytes: rollup.bytes,
allocated: rollup.allocated,
newest_mtime_ns: rollup.newest_mtime_ns,
by_ext,
}
}
fn named_partitions(&self, rollup: &InternedPartitionRollUp) -> PartitionRollUp {
PartitionRollUp {
all: self.named_rollup(&rollup.all),
unignored: self.named_rollup(&rollup.unignored),
}
}
fn contribution(&self, id: EntryId) -> InternedPartitionRollUp {
let entry = self.entry(id);
match entry.kind {
EntryKind::Dir => {
let mut all = entry.rollup().all.clone();
all.dirs += 1;
let mut unignored = InternedRollUp::default();
if !entry.ignored {
unignored = entry.rollup().unignored.clone();
unignored.dirs += 1;
}
InternedPartitionRollUp { all, unignored }
}
EntryKind::File => Self::file_contribution(&entry.attrs, entry.ext_id, entry.ignored),
EntryKind::Symlink | EntryKind::Other => InternedPartitionRollUp::default(),
}
}
fn file_contribution(
attrs: &Attrs,
ext_id: Option<ExtId>,
ignored: bool,
) -> InternedPartitionRollUp {
let mut all = InternedRollUp {
files: 1,
dirs: 0,
bytes: attrs.size,
allocated: attrs.allocated,
newest_mtime_ns: attrs.mtime_ns,
by_ext: BTreeMap::new(),
};
if let Some(ext_id) = ext_id {
all.by_ext.insert(
ext_id,
ExtTally { files: 1, bytes: attrs.size, allocated: attrs.allocated },
);
}
let unignored = if ignored { InternedRollUp::default() } else { all.clone() };
InternedPartitionRollUp { all, unignored }
}
fn merge_upward(
&mut self,
from_parent: Option<EntryId>,
contribution: &InternedPartitionRollUp,
) {
let mut current = from_parent;
while let Some(id) = current {
crate::counters::bump(|c| c.rollup_merges += 1);
let entry = self.entry_mut(id);
entry.rollup_mut().merge(contribution);
current = entry.parent;
}
}
fn unmerge_upward(
&mut self,
from_parent: Option<EntryId>,
contribution: &InternedPartitionRollUp,
) {
let mut current = from_parent;
while let Some(id) = current {
let entry = self.entry_mut(id);
entry.rollup_mut().unmerge(contribution);
current = entry.parent;
}
}
fn recompute_newest_upward(&mut self, from: Option<EntryId>) {
let mut current = from;
while let Some(id) = current {
let mut newest: Option<i64> = None;
for child in self.child_ids(id) {
let child_entry = self.entry(child);
let candidate = match child_entry.kind {
EntryKind::Dir => (child_entry.rollup().files > 0)
.then_some(child_entry.rollup().newest_mtime_ns),
EntryKind::File => Some(child_entry.attrs.mtime_ns),
EntryKind::Symlink | EntryKind::Other => None,
};
if let Some(candidate) = candidate {
newest = Some(newest.map_or(candidate, |current| current.max(candidate)));
}
}
let newest = newest.unwrap_or(0);
self.entry_mut(id).rollup_mut().all.newest_mtime_ns = newest;
let mut newest_unignored: Option<i64> = None;
for child in self.child_ids(id) {
let child_entry = self.entry(child);
let candidate = match child_entry.kind {
EntryKind::Dir => (!child_entry.ignored
&& child_entry.rollup().unignored.files > 0)
.then_some(child_entry.rollup().unignored.newest_mtime_ns),
EntryKind::File => (!child_entry.ignored).then_some(child_entry.attrs.mtime_ns),
EntryKind::Symlink | EntryKind::Other => None,
};
if let Some(candidate) = candidate {
newest_unignored =
Some(newest_unignored.map_or(candidate, |current| current.max(candidate)));
}
}
let entry = self.entry_mut(id);
entry.rollup_mut().unignored.newest_mtime_ns = newest_unignored.unwrap_or(0);
current = entry.parent;
}
}
fn resolve_dir_chain(&self, parts: &[&OsStr]) -> EntryId {
let mut current = EntryId::ROOT;
for part in parts {
current = self
.child(current, part)
.expect("validated ancestry remains present under the writer lock");
debug_assert!(self.entry(current).kind.is_dir());
}
current
}
fn apply_upsert<C: ConsequenceSink>(
&mut self,
path: &Path,
kind: EntryKind,
attrs: Attrs,
stats: &mut ApplyStats,
effects: &mut C,
parent_memo: &mut ParentMemo,
) -> bool {
crate::counters::bump(|c| c.upserts += 1);
if let (Some(dir), Some(name)) = (path.parent(), path.file_name()) {
if let Some(parent) = parent_memo.get(dir) {
crate::counters::bump(|c| c.parent_memo_hits += 1);
return self.upsert_beneath(parent, name, path, kind, attrs, stats, effects);
}
}
crate::counters::bump(|c| c.parent_resolutions += 1);
let Some(parts) = normalize(path) else {
return false;
};
let source = self.applying_source;
let Some((name, ancestors)) = parts.split_last() else {
if self.entry(EntryId::ROOT).attrs == attrs {
self.entry_mut(EntryId::ROOT).source = source;
stats.unchanged += 1;
return false;
}
let root = self.entry_mut(EntryId::ROOT);
let previous = root.attrs;
root.attrs = attrs;
root.source = source;
Self::bump_revision(root);
stats.updated += 1;
effects.change(|| EffectiveChange::Updated {
path: PathBuf::new(),
kind: EntryKind::Dir,
previous,
current: attrs,
});
return true;
};
let parent = self.resolve_dir_chain(ancestors);
if let Some(dir) = path.parent() {
parent_memo.set(dir, parent);
}
self.upsert_beneath(parent, name, path, kind, attrs, stats, effects)
}
#[allow(clippy::too_many_arguments)]
fn upsert_beneath<C: ConsequenceSink>(
&mut self,
parent: EntryId,
name: &OsStr,
path: &Path,
kind: EntryKind,
attrs: Attrs,
stats: &mut ApplyStats,
effects: &mut C,
) -> bool {
let source = self.applying_source;
let existing = self.child(parent, name);
if let Some(id) = existing {
let entry = self.entry(id);
if entry.kind == kind {
if entry.attrs == attrs {
self.entry_mut(id).source = source;
stats.unchanged += 1;
return false;
}
if kind.is_dir() {
let entry = self.entry_mut(id);
let previous = entry.attrs;
entry.attrs = attrs;
entry.source = source;
Self::bump_revision(entry);
stats.updated += 1;
effects.change(|| EffectiveChange::Updated {
path: path.to_path_buf(),
kind,
previous,
current: attrs,
});
return true;
}
let previous_attrs = entry.attrs;
self.invalidate_content(path);
if kind == EntryKind::File {
self.remove_serving_file_semantics(path, id, previous_attrs);
}
self.remove_serving_entry(path, kind, previous_attrs, id);
let old = self.contribution(id);
self.unmerge_upward(Some(parent), &old);
let entry = self.entry_mut(id);
let previous = entry.attrs;
entry.attrs = attrs;
entry.source = source;
Self::bump_revision(entry);
let new = self.contribution(id);
self.merge_upward(Some(parent), &new);
self.insert_serving_entry(path, kind, attrs, id);
if new.newest_mtime_ns < old.newest_mtime_ns {
self.recompute_newest_upward(Some(parent));
}
stats.updated += 1;
effects.change(|| EffectiveChange::Updated {
path: path.to_path_buf(),
kind,
previous,
current: attrs,
});
return true;
}
self.remove_entry(id, stats, effects);
}
let ext_id =
(kind == EntryKind::File).then(|| self.intern_ext(&crate::classify::ext_bucket(name)));
let ignored =
self.entry(parent).ignored || self.controls.matcher_for(path).is_ignored(kind.is_dir());
let id = self.alloc(Entry::new(
NewEntry {
parent: Some(parent),
name: name.to_os_string(),
ext_id,
ignored,
source,
kind,
attrs,
},
false,
));
self.insert_child(parent, name.to_os_string(), id);
let contribution = self.contribution(id);
self.merge_upward(Some(parent), &contribution);
stats.inserted += 1;
effects.change(|| EffectiveChange::Inserted { path: path.to_path_buf(), kind, attrs });
self.insert_serving_entry(path, kind, attrs, id);
true
}
pub(crate) fn insert_loaded_child(
&mut self,
parent: EntryId,
name: OsString,
kind: EntryKind,
attrs: Attrs,
) -> Option<EntryId> {
let parent_entry = self.try_entry(parent)?;
if parent_entry.kind != EntryKind::Dir || self.child(parent, &name).is_some() {
return None;
}
let source = self.applying_source;
let ext_id =
(kind == EntryKind::File).then(|| self.intern_ext(&crate::classify::ext_bucket(&name)));
let id = self.alloc(Entry::new(
NewEntry {
parent: Some(parent),
name: name.clone(),
ext_id,
ignored: false,
source,
kind,
attrs,
},
true,
));
self.insert_child(parent, name, id);
let contribution = self.contribution(id);
self.merge_upward(Some(parent), &contribution);
if self.serving.is_some() {
let path = self.path_of(id).expect("a newly loaded entry has a path");
self.insert_serving_entry(&path, kind, attrs, id);
}
Some(id)
}
fn apply_remove<C: ConsequenceSink>(
&mut self,
path: &Path,
stats: &mut ApplyStats,
effects: &mut C,
) -> bool {
let Some(id) = self.lookup(path) else {
stats.unchanged += 1;
return false;
};
if id == EntryId::ROOT {
stats.unchanged += 1;
return false;
}
self.remove_entry(id, stats, effects);
true
}
fn remove_entry<C: ConsequenceSink>(
&mut self,
id: EntryId,
stats: &mut ApplyStats,
effects: &mut C,
) {
let removed_root = self.path_of(id).expect("a live entry has a path");
self.invalidate_content(&removed_root);
self.remove_serving_subtree_semantics(id, &removed_root);
let parent = self.entry(id).parent;
let name = self.entry(id).name.clone();
let contribution = self.contribution(id);
self.unmerge_upward(parent, &contribution);
if let Some(parent) = parent {
self.remove_child(parent, &name);
}
let mut queue = VecDeque::from([(id, removed_root)]);
while let Some((node, path)) = queue.pop_front() {
let entry = self.entry(node);
let kind = entry.kind;
let attrs = entry.attrs;
let children: Vec<(OsString, EntryId)> = self
.children_of(node)
.expect("removed subtree entry is live")
.map(|(name, child)| (name.to_os_string(), child))
.collect();
let ext_id = entry.ext_id;
for (name, child) in children {
queue.push_back((child, path.join(name)));
}
self.remove_serving_entry(&path, kind, attrs, node);
effects.change(|| EffectiveChange::Removed { path, kind, attrs });
if let Some(ext_id) = ext_id {
self.release_ext(ext_id);
}
self.free(node);
stats.removed += 1;
}
self.recompute_newest_upward(parent);
}
fn invalidate_content(&mut self, path: &Path) {
if let Some(content) = self.content.as_mut() {
content.invalidate(path);
}
}
}
pub(crate) fn collect_child_expectations(
index: &Index,
path: &Path,
) -> BTreeMap<OsString, PathExpectation> {
index.children(path).map_or_else(BTreeMap::new, |children| {
children
.map(|(name, id)| {
let entry = index.entry(id);
let expectation = PathExpectation::new(
PathState::Present { kind: entry.kind, attrs: entry.attrs },
Some(index.identity(id)),
None,
);
(name.to_os_string(), expectation)
})
.collect()
})
}
#[derive(Default)]
struct ParentMemo {
entry: Option<(PathBuf, EntryId)>,
}
impl ParentMemo {
fn get(&self, dir: &Path) -> Option<EntryId> {
self.entry.as_ref().filter(|(cached, _)| cached == dir).map(|&(_, id)| id)
}
fn set(&mut self, dir: &Path, id: EntryId) {
match &mut self.entry {
Some((cached, cached_id)) => {
cached.clear();
cached.push(dir);
*cached_id = id;
}
slot => *slot = Some((dir.to_path_buf(), id)),
}
}
fn clear(&mut self) {
self.entry = None;
}
}
#[derive(Default)]
struct StructuralOverlay {
entries: HashMap<PathBuf, EntryKind>,
removed_roots: Vec<PathBuf>,
}
impl StructuralOverlay {
fn kind(&self, index: &Index, path: &Path) -> Option<EntryKind> {
self.entries.get(path).copied().or_else(|| {
(!self.removed_roots.iter().any(|removed| path.starts_with(removed)))
.then(|| index.kind(path))
.flatten()
})
}
fn upsert(&mut self, index: &Index, path: &Path, kind: EntryKind) {
if self.kind(index, path).is_some_and(|current| current != kind) {
self.remove(index, path);
}
self.entries.insert(path.to_path_buf(), kind);
}
fn remove(&mut self, index: &Index, path: &Path) {
if self.kind(index, path).is_none() {
return;
}
self.entries.retain(|candidate, _| !candidate.starts_with(path));
self.removed_roots.retain(|candidate| !candidate.starts_with(path));
if !self.removed_roots.iter().any(|removed| path.starts_with(removed)) {
self.removed_roots.push(path.to_path_buf());
}
}
}
fn normalize(path: &Path) -> Option<Vec<&OsStr>> {
let mut parts = Vec::new();
for component in path.components() {
match component {
Component::Normal(part) => parts.push(part),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
}
}
Some(parts)
}
fn prepare_observation(observation: &Observation) -> crate::Result<PreparedObservation> {
let mut ops = Vec::with_capacity(observation.len());
for observed in &observation.ops {
let path = canonical_relative_path(observed.op.path())?;
let op = match &observed.op {
Op::Upsert { kind, attrs, .. } => Op::Upsert { path, kind: *kind, attrs: *attrs },
Op::Remove { .. } => Op::Remove { path },
Op::ControlUpsert { source, .. } => {
if !crate::control::is_control_file(&path) {
return Err(crate::Error::InvalidControlPath(path));
}
Op::ControlUpsert { path, source: source.clone() }
}
Op::ControlRemove { .. } => {
if !crate::control::is_control_file(&path) {
return Err(crate::Error::InvalidControlPath(path));
}
Op::ControlRemove { path }
}
Op::InvalidateSubtree { reason, .. } => Op::InvalidateSubtree { path, reason: *reason },
};
ops.push(ObservationOp { op, expectation: observed.expectation });
}
Ok(PreparedObservation {
ops,
ancestry: PreparedAncestry::General,
#[cfg(test)]
reject_before_apply: false,
})
}
fn canonical_relative_path(path: &Path) -> crate::Result<PathBuf> {
let mut canonical = PathBuf::with_capacity(path.as_os_str().as_encoded_bytes().len());
for component in path.components() {
match component {
Component::Normal(part) => canonical.push(part),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(crate::Error::PathEscapesRoot(path.to_path_buf()));
}
}
}
Ok(canonical)
}
fn derive_impact(changes: &[EffectiveChange], state: &[StateTransition]) -> Impact {
let mut domains = BTreeSet::new();
let mut paths = BTreeSet::new();
let mut all_dirty = false;
let mut ancestor_visits = 0_u64;
let count_impact = crate::counters::enabled();
for change in changes {
match change {
EffectiveChange::Inserted { .. } | EffectiveChange::Removed { .. } => {
domains.extend([
ImpactDomain::Topology,
ImpactDomain::Metadata,
ImpactDomain::Classification,
ImpactDomain::Aggregates,
ImpactDomain::Content,
]);
}
EffectiveChange::Updated { .. } => {
domains.extend([
ImpactDomain::Metadata,
ImpactDomain::Aggregates,
ImpactDomain::Content,
]);
}
EffectiveChange::ControlUpdated { .. } | EffectiveChange::Reclassified { .. } => {
domains.extend([ImpactDomain::Classification, ImpactDomain::Aggregates]);
}
EffectiveChange::ControlRefusalUpdated { .. } => {
domains.insert(ImpactDomain::Classification);
}
EffectiveChange::Invalidated { .. } => {
domains.insert(ImpactDomain::State);
}
}
insert_dirty_ancestors(
change.path(),
&mut paths,
&mut all_dirty,
count_impact,
&mut ancestor_visits,
);
}
for transition in state {
domains.insert(ImpactDomain::State);
insert_dirty_ancestors(
transition.path(),
&mut paths,
&mut all_dirty,
count_impact,
&mut ancestor_visits,
);
}
if count_impact {
let candidates =
u64::try_from(changes.len().saturating_add(state.len())).unwrap_or(u64::MAX);
let retained_dirty_paths = u64::try_from(paths.len()).unwrap_or(u64::MAX);
crate::counters::bump(|counts| {
counts.impact_candidates = counts.impact_candidates.saturating_add(candidates);
counts.impact_ancestor_visits =
counts.impact_ancestor_visits.saturating_add(ancestor_visits);
counts.impact_retained_dirty_paths =
counts.impact_retained_dirty_paths.saturating_add(retained_dirty_paths);
counts.impact_all_dirty = counts.impact_all_dirty.saturating_add(u64::from(all_dirty));
});
}
Impact {
domains: domains.into_iter().collect(),
dirty_paths: if all_dirty { Vec::new() } else { paths.into_iter().collect() },
all_dirty,
}
}
fn commit_work(observations: u64, stats: ApplyStats) -> Work {
Work {
observations,
unchanged: stats.unchanged,
stale: stats.stale,
resource_refused: stats.resource_refused,
..Work::default()
}
}
fn insert_dirty_ancestors(
path: &Path,
paths: &mut BTreeSet<PathBuf>,
all_dirty: &mut bool,
count_impact: bool,
ancestor_visits: &mut u64,
) {
if *all_dirty {
return;
}
for ancestor in path.ancestors() {
if count_impact {
*ancestor_visits = ancestor_visits.saturating_add(1);
}
paths.insert(ancestor.to_path_buf());
if paths.len() > MAX_DIRTY_PATHS {
paths.clear();
*all_dirty = true;
return;
}
}
}
fn same_target(
current: Option<EntryIdentity>,
expected: Option<EntryIdentity>,
require_structure: bool,
) -> bool {
match (current, expected) {
(Some(current), Some(expected)) => current.same_target(expected, require_structure),
(None, None) => true,
(Some(_), None) | (None, Some(_)) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine_contract::ObservationOp;
use std::sync::{Arc, Barrier};
#[test]
fn reusable_entry_keeps_directory_state_out_of_line() {
let entry_bytes = std::mem::size_of::<Entry>();
let slot_bytes = std::mem::size_of::<Slot>();
assert!(
entry_bytes <= 136,
"common entry storage must not inline directory-only maps and roll-ups: {entry_bytes} bytes"
);
assert!(
slot_bytes <= entry_bytes + 16,
"the arena slot must not add a second per-entry allocation: entry={entry_bytes}, slot={slot_bytes}"
);
}
#[test]
fn detached_children_store_each_name_once_and_promote_on_mutation() {
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
);
builder
.push_directory(&mut crate::scan::DetachedDirectory {
path: PathBuf::new(),
children: vec![
crate::scan::DetachedChild {
name: OsString::from("dir"),
kind: EntryKind::Dir,
attrs: Attrs::default(),
position: 0,
},
crate::scan::DetachedChild {
name: OsString::from("z.txt"),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
position: 1,
},
],
control: None,
})
.expect("detached root listing");
builder
.push_directory(&mut crate::scan::DetachedDirectory {
path: PathBuf::from("dir"),
children: vec![
crate::scan::DetachedChild {
name: OsString::from("z.txt"),
kind: EntryKind::File,
attrs: file_attrs(2, 2),
position: 0,
},
crate::scan::DetachedChild {
name: OsString::from("a.txt"),
kind: EntryKind::File,
attrs: file_attrs(3, 3),
position: 1,
},
],
control: None,
})
.expect("detached child listing");
let mut index = builder.finish();
let directory = index.lookup(Path::new("dir")).expect("detached directory");
assert!(index.entry(EntryId::ROOT).directory().children.is_sorted());
assert!(index.entry(directory).directory().children.is_sorted());
assert_eq!(
index
.children(Path::new("dir"))
.expect("directory children")
.map(|(name, _)| name.to_os_string())
.collect::<Vec<_>>(),
[OsString::from("a.txt"), OsString::from("z.txt")]
);
index.apply_ok(&Observation::new(vec![upsert(
"dir/m.txt",
EntryKind::File,
file_attrs(4, 4),
)]));
assert!(index.entry(EntryId::ROOT).directory().children.is_sorted());
assert!(index.entry(directory).directory().children.is_mutable());
assert_eq!(
index
.children(Path::new("dir"))
.expect("directory children")
.map(|(name, _)| name.to_os_string())
.collect::<Vec<_>>(),
[OsString::from("a.txt"), OsString::from("m.txt"), OsString::from("z.txt")]
);
}
#[test]
fn a_folded_builder_keeps_the_largest_files_and_leaves_folded_names_to_the_walker() {
let child = |name: &str, kind, size, position| crate::scan::DetachedChild {
name: OsString::from(name),
kind,
attrs: file_attrs(size, 1),
position,
};
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
)
.folding(crate::execution::TreeRetention {
largest_files: 2,
size: crate::query::SizeMetric::Apparent,
});
let mut root = crate::scan::DetachedDirectory {
path: PathBuf::new(),
children: vec![
child("small.log", EntryKind::File, 1, 0),
child("dir", EntryKind::Dir, 0, 1),
child("big.bin", EntryKind::File, 50, 2),
child("twice.txt", EntryKind::File, 3, 3),
child("twice.txt", EntryKind::File, 40, 4),
child("mid.rs", EntryKind::File, 5, 5),
],
control: Some(Op::ControlUpsert {
path: PathBuf::from(".gitignore"),
source: b"*.log\n".to_vec(),
}),
};
builder.push_directory(&mut root).expect("root listing");
let left: Vec<_> =
root.children.iter().filter(|child| !child.name.is_empty()).map(|c| &c.name).collect();
assert_eq!(left, [&OsString::from("small.log")]);
let index = builder.finish();
assert!(index.is_folded());
assert_eq!(index.len(), 4, "the root, the directory, and the two largest files");
assert_eq!(
index
.children(Path::new(""))
.expect("root children")
.map(|(name, _)| name.to_os_string())
.collect::<Vec<_>>(),
[OsString::from("big.bin"), OsString::from("dir"), OsString::from("twice.txt")],
"kept files join their directory in name order"
);
assert_eq!(index.attrs(Path::new("twice.txt")), Some(&file_attrs(40, 1)));
let total = index.total();
assert_eq!((total.files, total.dirs, total.bytes), (4, 1, 96));
assert!(total.by_ext.is_empty(), "a folded index keeps no extension tallies");
assert_eq!(
index.partition_total().expect("observed").unignored.bytes,
95,
"the folded ignored file left the unignored partition"
);
assert_eq!(
index.folded_children(EntryId::ROOT),
Some(FoldedFiles {
files: 2,
bytes: 6,
allocated: 1_024,
ignored: crate::query::IgnoredSize { bytes: 1, allocated: 512 },
})
);
assert!(index.children_classification_known(Path::new("")));
let dir = index.lookup(Path::new("dir")).expect("the directory");
assert_eq!(index.folded_children(dir), None, "nothing was folded there");
}
#[test]
fn a_displaced_file_folds_into_its_own_directory() {
let child = |name: &str, kind, size, position| crate::scan::DetachedChild {
name: OsString::from(name),
kind,
attrs: file_attrs(size, 1),
position,
};
let listing = |path: &str, children| crate::scan::DetachedDirectory {
path: PathBuf::from(path),
children,
control: None,
};
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
)
.folding(crate::execution::TreeRetention {
largest_files: 1,
size: crate::query::SizeMetric::Apparent,
});
builder
.push_directory(&mut listing(
"",
vec![child("first", EntryKind::File, 10, 0), child("dir", EntryKind::Dir, 0, 1)],
))
.expect("root listing");
builder
.push_directory(&mut listing("dir", vec![child("larger", EntryKind::File, 20, 0)]))
.expect("nested listing");
let index = builder.finish();
assert_eq!(
index.folded_children(EntryId::ROOT),
Some(FoldedFiles { files: 1, bytes: 10, allocated: 512, ..FoldedFiles::default() })
);
let dir = index.lookup(Path::new("dir")).expect("the directory");
assert_eq!(index.folded_children(dir), None);
assert_eq!(index.kind(Path::new("dir/larger")), Some(EntryKind::File));
assert_eq!(index.kind(Path::new("first")), None);
assert_eq!(index.total().bytes, 30, "both files count in the totals");
}
#[test]
fn detached_builder_tolerates_a_duplicate_readdir_name() {
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
);
let twice = |position, mtime_ns| crate::scan::DetachedChild {
name: OsString::from("twice.txt"),
kind: EntryKind::File,
attrs: file_attrs(1, mtime_ns),
position,
};
let result = builder.push_directory(&mut crate::scan::DetachedDirectory {
path: PathBuf::new(),
children: vec![twice(0, 1), twice(1, 2)],
control: None,
});
assert!(result.is_ok(), "a duplicate listing name must not fail the scan: {result:?}");
let detached = builder.finish();
assert_eq!(detached.total().files, 1);
assert_eq!(detached.attrs(Path::new("twice.txt")), Some(&file_attrs(1, 2)));
let mut streaming = Index::new("/root");
streaming
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![
upsert("twice.txt", EntryKind::File, file_attrs(1, 1)),
upsert("twice.txt", EntryKind::File, file_attrs(1, 2)),
]))
.expect("streaming tolerates a re-upsert");
assert_eq!(streaming.total(), detached.total());
assert_eq!(streaming.attrs(Path::new("twice.txt")), detached.attrs(Path::new("twice.txt")));
}
#[test]
fn detached_builder_accepts_the_repeated_walk_of_a_duplicated_directory() {
let child = |name: &str, kind, attrs, position| crate::scan::DetachedChild {
name: OsString::from(name),
kind,
attrs,
position,
};
let listing = |path: &str, children| crate::scan::DetachedDirectory {
path: PathBuf::from(path),
children,
control: None,
};
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
);
builder
.push_directory(&mut listing(
"",
vec![
child("dir", EntryKind::Dir, file_attrs(0, 1), 0),
child("swapped", EntryKind::Dir, file_attrs(0, 1), 1),
child("dir", EntryKind::Dir, file_attrs(0, 2), 2),
child("swapped", EntryKind::File, file_attrs(5, 2), 3),
],
))
.expect("root listing with repeated names");
for _ in 0..2 {
builder
.push_directory(&mut listing(
"dir",
vec![child("nested", EntryKind::Dir, file_attrs(0, 3), 0)],
))
.expect("each walk of the repeated directory");
builder
.push_directory(&mut listing(
"dir/nested",
vec![child("file.txt", EntryKind::File, file_attrs(4, 4), 0)],
))
.expect("each walk below the repeated directory");
}
builder
.push_directory(&mut listing(
"swapped",
vec![child("stale.txt", EntryKind::File, file_attrs(6, 5), 0)],
))
.expect("the superseded directory's walk");
let error = builder
.push_directory(&mut listing("elsewhere", Vec::new()))
.expect_err("a listing whose parent was never listed");
assert!(matches!(
error,
crate::Error::UnknownAncestry { path, .. } if path == Path::new("elsewhere")
));
let index = builder.finish();
assert_eq!(index.attrs(Path::new("dir")), Some(&file_attrs(0, 2)));
assert_eq!(index.kind(Path::new("swapped")), Some(EntryKind::File));
assert!(index.lookup(Path::new("swapped/stale.txt")).is_none());
let total = index.total();
assert_eq!((total.files, total.dirs, total.bytes), (2, 2, 9));
}
#[test]
fn detached_builder_drains_an_applied_listing_for_its_worker() {
let child = |name: &str, kind, position| crate::scan::DetachedChild {
name: OsString::from(name),
kind,
attrs: file_attrs(1, 1),
position,
};
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
);
let mut root = crate::scan::DetachedDirectory {
path: PathBuf::new(),
children: vec![child("z.txt", EntryKind::File, 0), child("dir", EntryKind::Dir, 1)],
control: Some(Op::ControlUpsert {
path: PathBuf::from(".gitignore"),
source: b"*.log\n".to_vec(),
}),
};
let (buffer, capacity) = (root.children.as_ptr(), root.children.capacity());
builder.push_directory(&mut root).expect("root listing");
assert_eq!(root.path, PathBuf::new());
assert!(root.children.is_empty());
assert_eq!((root.children.as_ptr(), root.children.capacity()), (buffer, capacity));
assert!(root.control.is_none(), "the control is applied, not left to be applied twice");
let mut orphan = crate::scan::DetachedDirectory {
path: PathBuf::from("elsewhere"),
children: vec![child("kept.txt", EntryKind::File, 0)],
control: None,
};
builder.push_directory(&mut orphan).expect_err("a listing whose parent was never listed");
assert_eq!(orphan.children.len(), 1);
let index = builder.finish();
assert_eq!(index.kind(Path::new("dir")), Some(EntryKind::Dir));
assert_eq!(index.kind(Path::new("z.txt")), Some(EntryKind::File));
assert_eq!(index.total().files, 1);
}
fn file_attrs(size: u64, mtime_ns: i64) -> Attrs {
Attrs {
size,
allocated: size.div_ceil(512) * 512,
mtime_ns,
ctime_ns: mtime_ns,
inode: size.wrapping_mul(31).wrapping_add(mtime_ns.unsigned_abs()),
dev: 1,
}
}
fn upsert(path: &str, kind: EntryKind, attrs: Attrs) -> Op {
Op::Upsert { path: PathBuf::from(path), kind, attrs }
}
fn assert_serving_indexes(index: &Index) {
let serving = index.serving.as_ref().expect("test index has serving state");
let mut entries = BTreeMap::new();
let mut children = BTreeMap::<PathBuf, PortableChildren>::new();
let mut recent_files = BTreeSet::new();
let mut semantic_by_directory =
BTreeMap::<EntryId, (BTreeMap<String, ExtTally>, BTreeMap<String, ExtTally>)>::new();
let declared_exact_names: BTreeSet<_> =
index.types.exact_filenames().map(str::to_ascii_lowercase).collect();
let mut exact_name_by_directory =
BTreeMap::<EntryId, (BTreeMap<String, ExtTally>, BTreeMap<String, ExtTally>)>::new();
let mut semantic_refcounts = BTreeMap::<String, u64>::new();
let mut pending = vec![(EntryId::ROOT, PathBuf::new(), vec![EntryId::ROOT])];
while let Some((parent_id, parent_path, ancestors)) = pending.pop() {
let facts: Vec<_> = index
.children_of(parent_id)
.expect("live directory")
.map(|(name, id)| (name.to_os_string(), id))
.collect();
for (name, id) in facts {
let path = parent_path.join(&name);
let kind = index.kind_of(id).expect("live child");
let portable = crate::opened::read::portable_path(&path);
entries.insert(portable.clone(), id);
if kind == EntryKind::File {
recent_files.insert(RecentKey {
mtime_ns: index.attrs(&path).expect("live child has attributes").mtime_ns,
portable_path: portable,
id,
});
}
if kind == EntryKind::File {
let semantic = index.classify(&path).file_type.as_str().to_string();
*semantic_refcounts.entry(semantic.clone()).or_default() += 1;
let attrs = *index.attrs(&path).expect("live child has attributes");
let ignored = index.entry(id).ignored;
for ancestor in &ancestors {
let partition = semantic_by_directory.entry(*ancestor).or_default();
let all = partition.0.entry(semantic.clone()).or_default();
all.files += 1;
all.bytes += attrs.size;
all.allocated += attrs.allocated;
if !ignored {
let unignored = partition.1.entry(semantic.clone()).or_default();
unignored.files += 1;
unignored.bytes += attrs.size;
unignored.allocated += attrs.allocated;
}
}
if let Some(exact_name) = name
.to_str()
.map(str::to_ascii_lowercase)
.filter(|name| declared_exact_names.contains(name))
{
for ancestor in &ancestors {
let partition = exact_name_by_directory.entry(*ancestor).or_default();
let all = partition.0.entry(exact_name.clone()).or_default();
all.files += 1;
all.bytes += attrs.size;
all.allocated += attrs.allocated;
if !ignored {
let unignored = partition.1.entry(exact_name.clone()).or_default();
unignored.files += 1;
unignored.bytes += attrs.size;
unignored.allocated += attrs.allocated;
}
}
}
}
let partition = children.entry(parent_path.clone()).or_default();
let portable_name = crate::opened::read::portable_component(&name);
if kind.is_dir() {
partition.directories.insert(portable_name, id);
} else {
partition.nondirectories.insert(portable_name, id);
}
if kind.is_dir() {
let mut child_ancestors = ancestors.clone();
child_ancestors.insert(0, id);
pending.push((id, path, child_ancestors));
}
}
}
assert_eq!(serving.portable_entries, entries);
assert_eq!(serving.recent_files, recent_files);
let actual_semantics: BTreeMap<_, _> = serving
.semantic_by_directory
.iter()
.map(|(directory, partitions)| {
let named = |source: &BTreeMap<u32, ExtTally>| {
source
.iter()
.map(|(semantic, tally)| {
let name = serving.semantic_names[*semantic as usize]
.as_ref()
.expect("live semantic has a name")
.clone();
(name, *tally)
})
.collect()
};
(*directory, (named(&partitions.all), named(&partitions.unignored)))
})
.collect();
assert_eq!(actual_semantics, semantic_by_directory);
let actual_exact_names: BTreeMap<_, _> = serving
.exact_name_by_directory
.iter()
.map(|(directory, partitions)| {
let named = |source: &BTreeMap<u32, ExtTally>| {
source
.iter()
.map(|(exact_name, tally)| {
(serving.exact_names[*exact_name as usize].clone(), *tally)
})
.collect()
};
(*directory, (named(&partitions.all), named(&partitions.unignored)))
})
.collect();
assert_eq!(actual_exact_names, exact_name_by_directory);
assert_eq!(
serving.exact_names.iter().cloned().collect::<BTreeSet<_>>(),
declared_exact_names
);
assert_eq!(
serving.exact_name_ids,
serving
.exact_names
.iter()
.enumerate()
.map(|(position, name)| {
(
name.clone(),
u32::try_from(position).expect("the exact-name vocabulary fits u32"),
)
})
.collect()
);
assert!(serving.exact_name_by_directory.len() <= index.arena.len());
assert!(serving.exact_name_by_directory.values().all(|partitions| {
partitions.all.len() <= serving.exact_names.len()
&& partitions.unignored.len() <= serving.exact_names.len()
&& partitions
.all
.keys()
.chain(partitions.unignored.keys())
.all(|name| (*name as usize) < serving.exact_names.len())
}));
let actual_refcounts: BTreeMap<_, _> = serving
.semantic_ids
.iter()
.map(|(name, semantic)| (name.clone(), serving.semantic_refcounts[*semantic as usize]))
.collect();
assert_eq!(actual_refcounts, semantic_refcounts);
assert_eq!(serving.portable_children, children);
assert_eq!(
u64::try_from(serving.portable_entries.len()).expect("entry count fits u64"),
index.len().saturating_sub(1),
"every retained non-root entry has exactly one portable name"
);
}
#[test]
fn portable_indexes_conserve_insert_kind_change_and_subtree_removal() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/a", EntryKind::File, file_attrs(1, 1)),
upsert("replace", EntryKind::File, file_attrs(2, 2)),
]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![
upsert("replace", EntryKind::Dir, Attrs::default()),
upsert("replace/child", EntryKind::File, file_attrs(3, 3)),
]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![upsert("dir/a", EntryKind::File, file_attrs(4, 9))]));
assert_serving_indexes(&index);
assert_eq!(
index
.serving
.as_ref()
.expect("opened test index")
.recent_files
.iter()
.map(|entry| entry.portable_path.as_str())
.collect::<Vec<_>>(),
vec!["dir/a", "replace/child"]
);
index.apply_ok(&Observation::new(vec![
upsert("dir/current", EntryKind::Symlink, file_attrs(5, 5)),
upsert("dir/pipe", EntryKind::Other, file_attrs(6, 6)),
]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![
upsert("dir/current", EntryKind::Symlink, file_attrs(7, 7)),
upsert("dir/pipe", EntryKind::Other, file_attrs(8, 8)),
]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("replace") }]));
assert_serving_indexes(&index);
assert_eq!(
index.portable_entries().keys().map(crate::PortablePath::as_str).collect::<Vec<_>>(),
vec!["dir", "dir/a", "dir/current", "dir/pipe"]
);
}
#[test]
fn non_file_attrs_updates_leave_file_semantics_untouched() {
for kind in [EntryKind::Symlink, EntryKind::Other] {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![upsert("current", kind, file_attrs(1, 1))]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![upsert("current", kind, file_attrs(1, 2))]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![upsert(
"notes",
EntryKind::File,
file_attrs(5, 3),
)]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![upsert("current", kind, file_attrs(4, 4))]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("notes") }]));
assert_serving_indexes(&index);
}
}
#[test]
fn escaping_touches_only_invalid_bytes_and_percent() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/plain.txt", EntryKind::File, file_attrs(1, 1)),
upsert("café", EntryKind::Dir, Attrs::default()),
upsert("café/naïve.txt", EntryKind::File, file_attrs(2, 2)),
upsert("日本語.md", EntryKind::File, file_attrs(3, 3)),
upsert("a b", EntryKind::Dir, Attrs::default()),
upsert("a b/c d.txt", EntryKind::File, file_attrs(5, 5)),
upsert("100%.txt", EntryKind::File, file_attrs(4, 4)),
]));
let names: Vec<_> =
index.portable_entries().keys().map(crate::PortablePath::as_str).collect();
assert_eq!(
names,
vec![
"100%25.txt",
"a b",
"a b/c d.txt",
"café",
"café/naïve.txt",
"dir",
"dir/plain.txt",
"日本語.md",
],
"only the literal percent is rewritten; separators, spaces and non-ASCII are not"
);
}
#[test]
fn declared_exact_names_roll_up_by_ancestor_and_partition() {
let types = Arc::new(
crate::classify::TypeRegistry::from_manifest(
"[[kind]]\nid = \"make\"\nfamily = \"code\"\nfilenames = [\"Makefile\"]\n",
)
.expect("custom registry"),
);
let scope =
ScanScope { type_rules_fingerprint: types.fingerprint(), ..ScanScope::default() };
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
scope,
types,
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
upsert("Makefile", EntryKind::File, file_attrs(2, 1)),
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/makefile", EntryKind::File, file_attrs(3, 2)),
upsert("dir/notes", EntryKind::File, file_attrs(5, 3)),
]));
let serving = index.serving.as_ref().expect("opened test index");
let exact_name = serving.exact_name_ids["makefile"];
let root = &serving.exact_name_by_directory[&EntryId::ROOT];
assert_eq!(root.all[&exact_name], ExtTally { files: 2, bytes: 5, allocated: 1_024 });
assert_eq!(root.unignored, root.all);
let directory = index.lookup(Path::new("dir")).expect("directory");
let nested = &serving.exact_name_by_directory[&directory];
assert_eq!(nested.all[&exact_name], ExtTally { files: 1, bytes: 3, allocated: 512 });
assert_eq!(nested.unignored, nested.all);
}
#[test]
#[ignore = "manual opened-root commit-cost evidence"]
fn measure_opened_serving_commit_cost() {
const DIRECTORY_COUNT: usize = 100;
const FILES_PER_DIRECTORY: usize = 100;
const SAMPLE_COUNT: usize = 7;
let mut operations = Vec::with_capacity(
DIRECTORY_COUNT.saturating_mul(FILES_PER_DIRECTORY.saturating_add(1)),
);
for directory in 0..DIRECTORY_COUNT {
let parent = format!("d{directory:03}");
operations.push(upsert(&parent, EntryKind::Dir, Attrs::default()));
for file in 0..FILES_PER_DIRECTORY {
let size = u64::try_from(file).expect("the probe file count fits u64") + 1;
let mtime = i64::try_from(file).expect("the probe file count fits i64");
let name = if file == 0 {
format!("{parent}/Makefile")
} else {
format!("{parent}/f{file:03}.rs")
};
operations.push(upsert(&name, EntryKind::File, file_attrs(size, mtime)));
}
}
let observation = Observation::new(operations);
let types = crate::classify::TypeRegistry::compiled_shared();
let scope =
ScanScope { type_rules_fingerprint: types.fingerprint(), ..ScanScope::default() };
let measure = |opened: bool| {
let mut index = if opened {
Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
scope,
Arc::clone(&types),
DEFAULT_JOURNAL_CAPACITY_BYTES,
)
} else {
Index::new_with_scope_types_and_journal_capacity_bytes(
"/root",
scope,
Arc::clone(&types),
DEFAULT_JOURNAL_CAPACITY_BYTES,
)
};
let started = std::time::Instant::now();
index.apply_ok(&observation);
let elapsed = started.elapsed();
std::hint::black_box(index.len());
(elapsed, index)
};
let _ = measure(false);
let _ = measure(true);
let mut detached = Vec::with_capacity(SAMPLE_COUNT);
let mut opened = Vec::with_capacity(SAMPLE_COUNT);
let mut last_opened = None;
for sample in 0..SAMPLE_COUNT {
if sample % 2 == 0 {
detached.push(measure(false).0);
let (duration, index) = measure(true);
opened.push(duration);
last_opened = Some(index);
} else {
let (duration, index) = measure(true);
opened.push(duration);
last_opened = Some(index);
detached.push(measure(false).0);
}
}
detached.sort_unstable();
opened.sort_unstable();
let detached_median = detached[SAMPLE_COUNT / 2];
let opened_median = opened[SAMPLE_COUNT / 2];
let ratio = opened_median.as_secs_f64() / detached_median.as_secs_f64();
let index = last_opened.expect("an opened sample ran");
let serving = index.serving.as_ref().expect("opened sample has serving indexes");
let semantic_rows: usize = serving
.semantic_by_directory
.values()
.map(|partitions| partitions.all.len() + partitions.unignored.len())
.sum();
let exact_name_rows: usize = serving
.exact_name_by_directory
.values()
.map(|partitions| partitions.all.len() + partitions.unignored.len())
.sum();
eprintln!(
"entries={} detached_median_us={} opened_median_us={} ratio={ratio:.3} \
portable_rows={} child_rows={} recent_rows={} semantic_rows={} exact_name_rows={} \
exact_name_vocabulary={}",
index.len(),
detached_median.as_micros(),
opened_median.as_micros(),
serving.portable_entries.len(),
serving
.portable_children
.values()
.map(|children| children.directories.len() + children.nondirectories.len())
.sum::<usize>(),
serving.recent_files.len(),
semantic_rows,
exact_name_rows,
serving.exact_names.len(),
);
}
#[test]
fn detached_indexes_never_allocate_or_populate_serving_state() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/a", EntryKind::File, file_attrs(1, 1)),
]));
assert!(index.serving.is_none());
assert!(index.portable_children(Path::new("dir")).is_none());
}
#[test]
fn insert_loaded_child_skips_serving_path_when_serving_is_off() {
let mut index = Index::new("/root");
let id = index
.insert_loaded_child(
EntryId::ROOT,
OsString::from("a.rs"),
EntryKind::File,
file_attrs(4, 1),
)
.expect("parent is a live directory");
assert!(!index.serving_indexes_enabled());
assert_eq!(index.path_of(id), Some(PathBuf::from("a.rs")));
assert_eq!(index.lookup(Path::new("a.rs")), Some(id));
assert_eq!(index.total().files, 1);
}
#[test]
fn insert_loaded_child_fills_serving_when_enabled() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
let id = index
.insert_loaded_child(
EntryId::ROOT,
OsString::from("a.rs"),
EntryKind::File,
file_attrs(4, 1),
)
.expect("parent is a live directory");
assert!(index.serving_indexes_enabled());
assert_eq!(index.path_of(id), Some(PathBuf::from("a.rs")));
let serving = index.serving.as_ref().expect("opened test index");
assert!(serving.portable_entries.keys().any(|path| path.as_str() == "a.rs"));
}
#[test]
fn opened_entry_values_project_name_identity_without_retaining_it_on_detached_entries() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![upsert(
"bundle.umd.min.js",
EntryKind::File,
file_attrs(7, 1),
)]));
let row = index.entry_value(Path::new("bundle.umd.min.js")).expect("entry value");
let identity = row.classification.expect("regular files carry name identity");
assert_eq!(identity.logical_extension(), Some(".min.js"));
assert_eq!(identity.canonical_extension(), Some(".js"));
assert_eq!(identity.kind_id(), Some("javascript"));
assert_eq!(identity.content_family(), crate::classify::ContentFamily::Code);
}
#[test]
fn a_shared_snapshot_drops_opened_root_serving_state() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![upsert("a.txt", EntryKind::File, file_attrs(1, 1))]));
assert!(index.serving_indexes_enabled());
let snapshot = IndexHandle::new(index).snapshot().expect("detached snapshot");
assert!(!snapshot.serving_indexes_enabled());
assert_eq!(snapshot.total().files, 1);
}
#[test]
#[should_panic(expected = "an index's registry must match its semantic scope")]
fn an_index_cannot_claim_a_registry_different_from_its_scope() {
let types = Arc::new(
crate::classify::TypeRegistry::from_manifest(
"[[kind]]\nid = \"notes\"\nfamily = \"prose\"\nextensions = [\"rs\"]\n",
)
.expect("custom registry"),
);
let _ = Index::new_with_scope_and_types("/root", ScanScope::default(), types);
}
#[test]
fn parent_memo_does_not_survive_removing_the_directory_it_remembers() {
let mut index = Index::new(PathBuf::from("/root"));
index.apply_ok(&Observation::new(vec![
upsert("dir", EntryKind::Dir, file_attrs(0, 1)),
upsert("dir/a.txt", EntryKind::File, file_attrs(10, 1)),
]));
index.apply_ok(&Observation::new(vec![
upsert("dir/b.txt", EntryKind::File, file_attrs(20, 1)),
Op::Remove { path: PathBuf::from("dir") },
upsert("dir", EntryKind::Dir, file_attrs(0, 2)),
upsert("dir/c.txt", EntryKind::File, file_attrs(30, 2)),
]));
let children = index.children(Path::new("dir")).expect("dir survives");
let names: Vec<_> = children.map(|(name, _)| name.to_os_string()).collect();
assert_eq!(names, vec![OsString::from("c.txt")], "only the re-added child remains");
assert_eq!(index.total().bytes, 30, "totals match the surviving child");
}
#[test]
fn a_kind_change_mid_batch_leaves_the_memo_usable_for_the_next_sibling() {
let mut index = Index::new(PathBuf::from("/root"));
index.apply_ok(&Observation::new(vec![
upsert("swap", EntryKind::Dir, file_attrs(0, 1)),
upsert("swap/inner", EntryKind::Dir, file_attrs(0, 1)),
upsert("swap/inner/deep.txt", EntryKind::File, file_attrs(40, 1)),
]));
index.apply_ok(&Observation::new(vec![
upsert("swap/inner/other.txt", EntryKind::File, file_attrs(50, 1)),
upsert("swap/inner", EntryKind::File, file_attrs(60, 2)),
upsert("swap/sibling.txt", EntryKind::File, file_attrs(70, 2)),
]));
let children = index.children(Path::new("swap")).expect("swap survives");
let names: Vec<_> = children.map(|(name, _)| name.to_os_string()).collect();
assert_eq!(names, vec![OsString::from("inner"), OsString::from("sibling.txt")]);
assert_eq!(index.total().files, 2, "inner counts once, as a file");
assert_eq!(index.total().bytes, 130, "the dropped subtree's bytes are gone");
}
#[test]
fn parent_memo_distinguishes_directories_that_share_a_name_prefix() {
let mut index = Index::new(PathBuf::from("/root"));
index.apply_ok(&Observation::new(vec![
upsert("src", EntryKind::Dir, file_attrs(0, 1)),
upsert("src2", EntryKind::Dir, file_attrs(0, 1)),
upsert("src/one.txt", EntryKind::File, file_attrs(11, 1)),
upsert("src2/two.txt", EntryKind::File, file_attrs(22, 1)),
upsert("src/three.txt", EntryKind::File, file_attrs(33, 1)),
]));
let in_src: Vec<_> = index
.children(Path::new("src"))
.expect("src")
.map(|(name, _)| name.to_os_string())
.collect();
let in_src2: Vec<_> = index
.children(Path::new("src2"))
.expect("src2")
.map(|(name, _)| name.to_os_string())
.collect();
assert_eq!(in_src, vec![OsString::from("one.txt"), OsString::from("three.txt")]);
assert_eq!(in_src2, vec![OsString::from("two.txt")]);
}
#[test]
fn parent_memo_leaves_root_level_entries_alone() {
let mut index = Index::new(PathBuf::from("/root"));
index.apply_ok(&Observation::new(vec![
upsert("a.txt", EntryKind::File, file_attrs(5, 1)),
upsert("b.txt", EntryKind::File, file_attrs(6, 1)),
upsert("dir", EntryKind::Dir, file_attrs(0, 1)),
upsert("dir/c.txt", EntryKind::File, file_attrs(7, 1)),
upsert("d.txt", EntryKind::File, file_attrs(8, 1)),
]));
let top: Vec<_> = index
.children(Path::new(""))
.expect("root children")
.map(|(name, _)| name.to_os_string())
.collect();
assert_eq!(
top,
vec![
OsString::from("a.txt"),
OsString::from("b.txt"),
OsString::from("d.txt"),
OsString::from("dir"),
]
);
assert_eq!(index.total().files, 4);
assert_eq!(index.total().bytes, 26);
}
#[test]
fn shared_queries_return_owned_values_and_release_the_lock() {
let handle = IndexHandle::new(index_with_sample_tree());
let retained_total = handle.total().expect("total");
let retained_history = handle.since(Clock::ZERO).expect("history");
let retained_children = handle.children(Path::new("src")).expect("children");
let retained_snapshot = handle.snapshot().expect("snapshot");
let writer = handle.clone();
let (done_tx, done_rx) = std::sync::mpsc::sync_channel(1);
let thread = std::thread::spawn(move || {
let result = writer.apply(&Observation::new(vec![upsert(
"concurrent.txt",
EntryKind::File,
file_attrs(7, 30),
)]));
done_tx.send(result).expect("report writer result");
});
let outcome = done_rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("owned query results must not retain the read lock")
.expect("writer apply");
thread.join().expect("writer thread");
assert_eq!(outcome.inserted, 1);
assert_eq!(retained_total.files, 3);
assert!(!retained_history.commits.is_empty());
assert_eq!(retained_children.expect("src directory").len(), 2);
assert!(retained_snapshot.lookup(Path::new("concurrent.txt")).is_none());
assert!(handle.kind(Path::new("concurrent.txt")).expect("query").is_some());
}
#[test]
fn cloned_indexes_are_independent_detached_images() {
let original = index_with_sample_tree();
let original_clock = original.clock();
let mut detached = original.clone();
detached.apply_ok(&Observation::new(vec![upsert(
"detached-only.txt",
EntryKind::File,
file_attrs(7, 30),
)]));
assert_eq!(original.clock(), original_clock);
assert!(original.lookup(Path::new("detached-only.txt")).is_none());
assert!(detached.lookup(Path::new("detached-only.txt")).is_some());
assert_eq!(detached.total().files, original.total().files + 1);
}
#[test]
fn captured_child_expectations_match_individual_path_lookups() {
let index = index_with_sample_tree();
let captured = collect_child_expectations(&index, Path::new("src"));
assert!(!captured.is_empty());
for (name, expectation) in captured {
assert_eq!(expectation, index.expectation(&Path::new("src").join(name)));
}
}
#[test]
fn index_and_shared_handle_are_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Index>();
assert_send_sync::<IndexHandle>();
}
#[test]
fn simultaneous_writers_commit_unique_contiguous_clocks_in_journal_order() {
let writer_count: usize = 8;
let handle: IndexHandle = IndexHandle::new(Index::new("/root"));
let barrier: Arc<Barrier> = Arc::new(Barrier::new(writer_count));
let (result_tx, result_rx) = std::sync::mpsc::sync_channel(writer_count);
std::thread::scope(|scope| {
for worker_id in 0..writer_count {
let worker: IndexHandle = handle.clone();
let start: Arc<Barrier> = Arc::clone(&barrier);
let results = result_tx.clone();
scope.spawn(move || {
let ordinal: u64 = u64::try_from(worker_id + 1).expect("small worker count");
let path: String = format!("writer-{ordinal}.txt");
start.wait();
let outcome: crate::Result<ApplyOutcome> =
worker.apply(&Observation::new(vec![upsert(
&path,
EntryKind::File,
file_attrs(ordinal, i64::try_from(ordinal).expect("small ordinal")),
)]));
results.send((path, outcome)).expect("report writer result");
});
}
});
drop(result_tx);
let mut committed_clocks: Vec<u64> = Vec::with_capacity(writer_count);
for (path, outcome) in result_rx {
let commit = outcome.expect("writer apply").commit.expect("unique upsert must commit");
committed_clocks.push(commit.clock.0);
assert!(handle.kind(Path::new(&path)).expect("query committed path").is_some());
}
committed_clocks.sort_unstable();
let last_clock: u64 = u64::try_from(writer_count).expect("small writer count");
let expected_clocks: Vec<u64> = (1..=last_clock).collect();
assert_eq!(committed_clocks, expected_clocks);
assert_eq!(handle.clock().expect("clock"), Clock(last_clock));
let journal_clocks: Vec<u64> = handle
.since(Clock::ZERO)
.expect("journal")
.commits
.iter()
.map(|commit| commit.clock.0)
.collect();
assert_eq!(journal_clocks, expected_clocks);
}
#[test]
fn readers_observe_only_complete_states_around_a_large_batch() {
let file_count: u64 = 2_048;
let expected_bytes: u64 = file_count * (file_count + 1) / 2;
let operations: Vec<Op> =
std::iter::once(upsert("batch", EntryKind::Dir, file_attrs(0, 1)))
.chain((1..=file_count).map(|ordinal| {
upsert(
&format!("batch/file-{ordinal}.bin"),
EntryKind::File,
file_attrs(ordinal, i64::try_from(ordinal).expect("small ordinal")),
)
}))
.collect();
let observation: Observation = Observation::new(operations);
let handle: IndexHandle = IndexHandle::new(Index::new("/root"));
let before: Index = handle.snapshot().expect("before snapshot");
assert_eq!(before.total().files, 0);
let barrier: Arc<Barrier> = Arc::new(Barrier::new(2));
let (done_tx, done_rx) = std::sync::mpsc::sync_channel(1);
std::thread::scope(|scope| {
let writer: IndexHandle = handle.clone();
let writer_start: Arc<Barrier> = Arc::clone(&barrier);
scope.spawn(move || {
writer_start.wait();
let result: crate::Result<ApplyOutcome> = writer.apply(&observation);
done_tx.send(result).expect("report batch result");
});
let reader: IndexHandle = handle.clone();
let reader_start: Arc<Barrier> = Arc::clone(&barrier);
scope.spawn(move || {
reader_start.wait();
let deadline: std::time::Instant =
std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
assert!(
std::time::Instant::now() < deadline,
"reader did not observe batch completion before the deadline"
);
let image: Index = reader.snapshot().expect("coherent reader snapshot");
let total = image.total();
match total.files {
0 => {
assert_eq!(total.bytes, 0);
assert_eq!(image.len(), 1);
}
count if count == file_count => {
assert_eq!(total.bytes, expected_bytes);
assert_eq!(total.dirs, 1);
assert_eq!(image.len(), file_count + 2);
}
partial => panic!("reader observed partial batch with {partial} files"),
}
match done_rx.try_recv() {
Ok(result) => {
assert_eq!(result.expect("batch apply").inserted, file_count + 1);
break;
}
Err(std::sync::mpsc::TryRecvError::Empty) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
panic!("batch writer disconnected")
}
}
}
});
});
let after: Index = handle.snapshot().expect("after snapshot");
assert_eq!(after.total().files, file_count);
assert_eq!(after.total().bytes, expected_bytes);
assert_eq!(after.len(), file_count + 2);
}
#[test]
fn poisoned_shared_lock_returns_typed_errors() {
let handle: IndexHandle = IndexHandle::new(Index::new("/root"));
let poisoner: IndexHandle = handle.clone();
let panic_result: std::thread::Result<()> = std::thread::spawn(move || {
let _poison_guard = poisoner.write_index().expect("initial write lock");
panic!("intentional lock poison");
})
.join();
assert!(panic_result.is_err());
assert!(matches!(handle.total(), Err(crate::Error::IndexLockPoisoned)));
assert!(matches!(
handle.apply(&Observation::new(vec![upsert(
"never-applied.txt",
EntryKind::File,
file_attrs(1, 1),
)])),
Err(crate::Error::IndexLockPoisoned)
));
}
#[test]
fn clock_exhaustion_rejects_before_any_mutation() {
let mut index = index_with_sample_tree();
index.clock = Clock(u64::MAX);
let before_total = index.total();
let before_len = index.len();
let error = index
.apply(&Observation::new(vec![upsert(
"too-late.txt",
EntryKind::File,
file_attrs(1, 1),
)]))
.expect_err("clock exhaustion must be typed");
assert!(matches!(error, crate::Error::ClockExhausted));
assert_eq!(index.clock(), Clock(u64::MAX));
assert_eq!(index.len(), before_len);
assert_eq!(index.total(), before_total);
assert!(index.lookup(Path::new("too-late.txt")).is_none());
}
#[test]
fn terminal_clock_still_accepts_no_op_and_stale_observations() {
let mut index = index_with_sample_tree();
let current = *index.attrs(Path::new("src/main.rs")).expect("sample attributes");
let stale_baseline = index.expectation(Path::new("src/main.rs"));
index.apply_ok(&Observation::new(vec![upsert(
"src/main.rs",
EntryKind::File,
file_attrs(99, 99),
)]));
index.clock = Clock(u64::MAX);
let before_total = index.total();
let before_len = index.len();
let before_journal = index.journal.clone();
let no_op = index
.apply(&Observation::new(vec![upsert(
"src/main.rs",
EntryKind::File,
file_attrs(99, 99),
)]))
.expect("a no-op needs no new clock");
let stale = index
.apply(&Observation::from_ops(vec![ObservationOp::if_state(
upsert("src/main.rs", EntryKind::File, current),
stale_baseline,
)]))
.expect("a rejected stale observation needs no new clock");
assert_eq!(no_op.unchanged, 1);
assert!(no_op.commit.is_none());
assert_eq!(stale.stale, 1);
assert!(stale.commit.is_none());
assert_eq!(index.clock(), Clock(u64::MAX));
assert_eq!(index.len(), before_len);
assert_eq!(index.total(), before_total);
assert_eq!(index.journal, before_journal);
}
#[test]
fn delayed_conditional_observation_cannot_overwrite_newer_state() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(10, 1),
)]));
let baseline = index.expectation(Path::new("file.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("file.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(30, 3),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 1);
assert!(outcome.commit.is_none());
assert_eq!(index.attrs(Path::new("file.txt")).expect("file").size, 30);
}
#[test]
fn delayed_absent_child_cannot_replace_a_newer_parent_file() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("parent", EntryKind::Dir, file_attrs(0, 1))]));
let child_baseline = index.expectation(Path::new("parent/child.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("parent/child.txt", EntryKind::File, file_attrs(10, 2)),
child_baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"parent",
EntryKind::File,
file_attrs(20, 3),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 1);
assert!(outcome.commit.is_none());
assert_eq!(index.kind(Path::new("parent")), Some(EntryKind::File));
assert!(index.lookup(Path::new("parent/child.txt")).is_none());
}
#[test]
fn conditional_observation_rejects_present_state_aba() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(10, 1),
)]));
let baseline = index.expectation(Path::new("file.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("file.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(30, 3),
)]));
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(10, 1),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 1);
assert!(outcome.commit.is_none());
assert_eq!(index.attrs(Path::new("file.txt")).expect("file").size, 10);
}
#[test]
fn conditional_observation_rejects_absent_state_aba() {
let mut index = Index::new("/root");
let baseline = index.expectation(Path::new("file.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("file.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(30, 3),
)]));
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("file.txt") }]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 1);
assert!(outcome.commit.is_none());
assert!(index.lookup(Path::new("file.txt")).is_none());
}
#[test]
fn convergent_conditional_upsert_applies_as_unchanged_not_stale() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(10, 1),
)]));
let baseline = index.expectation(Path::new("file.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("file.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"file.txt",
EntryKind::File,
file_attrs(20, 2),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 0);
assert_eq!(outcome.stats.unchanged, 1);
assert!(outcome.commit.is_none());
assert_eq!(index.attrs(Path::new("file.txt")).expect("file").size, 20);
}
#[test]
fn convergent_conditional_remove_applies_as_unchanged_not_stale() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("dir", EntryKind::Dir, file_attrs(0, 1)),
upsert("dir/file.txt", EntryKind::File, file_attrs(10, 1)),
]));
let baseline = index.expectation(Path::new("dir/file.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
Op::Remove { path: PathBuf::from("dir/file.txt") },
baseline,
)]);
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("dir/file.txt") }]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 0);
assert_eq!(outcome.stats.unchanged, 1);
assert!(outcome.commit.is_none());
assert!(index.lookup(Path::new("dir/file.txt")).is_none());
}
#[test]
fn convergent_conditional_control_ops_apply_as_unchanged_not_stale() {
let path = PathBuf::from(".gitignore");
let rules =
|source: &[u8]| Op::ControlUpsert { path: path.clone(), source: source.to_vec() };
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(6, 1)),
rules(b"before"),
]));
let baseline = index.expectation(&path);
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(7, 2)),
rules(b"changed"),
]));
let outcome = index.apply_ok(&Observation::from_ops(vec![
ObservationOp::if_state(
upsert(".gitignore", EntryKind::File, file_attrs(7, 2)),
baseline,
),
ObservationOp::if_state(rules(b"changed"), baseline),
]));
assert_eq!(outcome.stats.stale, 0);
assert!(outcome.commit.is_none());
let diverged = index.apply_ok(&Observation::from_ops(vec![ObservationOp::if_state(
rules(b"other"),
baseline,
)]));
assert_eq!(diverged.stats.stale, 1);
assert!(index.controls().expect("control state observed").source_is(&path, b"changed"));
let baseline = index.expectation(&path);
index.apply_ok(&Observation::new(vec![Op::Remove { path: path.clone() }]));
let outcome = index.apply_ok(&Observation::from_ops(vec![
ObservationOp::if_state(Op::Remove { path: path.clone() }, baseline),
ObservationOp::if_state(Op::ControlRemove { path: path.clone() }, baseline),
]));
assert_eq!(outcome.stats.stale, 0);
assert!(outcome.commit.is_none());
assert!(!index.controls().expect("control state observed").contains(&path));
}
#[test]
fn unrelated_mutation_does_not_stale_an_absent_path() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("dir", EntryKind::Dir, file_attrs(0, 1))]));
let baseline = index.expectation(Path::new("dir/new.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("dir/new.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"other.txt",
EntryKind::File,
file_attrs(30, 3),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 0);
assert_eq!(outcome.stats.inserted, 1);
assert_eq!(index.attrs(Path::new("dir/new.txt")).expect("file").size, 20);
}
#[test]
fn directory_metadata_change_does_not_stale_an_absent_child() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("dir", EntryKind::Dir, file_attrs(0, 1))]));
let baseline = index.expectation(Path::new("dir/new.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("dir/new.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert("dir", EntryKind::Dir, file_attrs(0, 3))]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 0);
assert_eq!(outcome.stats.inserted, 1);
}
#[test]
fn file_parent_metadata_change_stales_an_absent_child() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"parent",
EntryKind::File,
file_attrs(10, 1),
)]));
let baseline = index.expectation(Path::new("parent/child.txt"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
upsert("parent/child.txt", EntryKind::File, file_attrs(20, 2)),
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"parent",
EntryKind::File,
file_attrs(30, 3),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 1);
assert_eq!(index.kind(Path::new("parent")), Some(EntryKind::File));
assert!(index.lookup(Path::new("parent/child.txt")).is_none());
}
#[test]
fn delayed_directory_remove_cannot_delete_a_newer_child() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("dir", EntryKind::Dir, file_attrs(0, 1))]));
let baseline = index.expectation(Path::new("dir"));
let delayed = Observation::from_ops(vec![ObservationOp::if_state(
Op::Remove { path: PathBuf::from("dir") },
baseline,
)]);
index.apply_ok(&Observation::new(vec![upsert(
"dir/new.txt",
EntryKind::File,
file_attrs(20, 2),
)]));
let outcome = index.apply_ok(&delayed);
assert_eq!(outcome.stats.stale, 1);
assert!(index.lookup(Path::new("dir/new.txt")).is_some());
}
#[test]
fn conditional_batch_is_validated_at_one_boundary() {
let mut index = Index::new("/root");
let first = index.expectation(Path::new("first.txt"));
let second = index.expectation(Path::new("second.txt"));
let outcome = index.apply_ok(&Observation::from_ops(vec![
ObservationOp::if_state(upsert("first.txt", EntryKind::File, file_attrs(10, 1)), first),
ObservationOp::if_state(
upsert("second.txt", EntryKind::File, file_attrs(20, 2)),
second,
),
]));
assert_eq!(outcome.stats.inserted, 2);
assert_eq!(outcome.stats.stale, 0);
}
#[test]
fn public_observation_outputs_use_canonical_encoded_paths() {
let separator = std::path::MAIN_SEPARATOR;
let dotted = PathBuf::from(format!("dotted{separator}.{separator}file.txt"));
let dotted_canonical = PathBuf::from(format!("dotted{separator}file.txt"));
let repeated = PathBuf::from(format!("repeated{separator}{separator}file.txt"));
let repeated_canonical = PathBuf::from(format!("repeated{separator}file.txt"));
let mut index = Index::new("/root");
let outcome = index.apply_ok(&Observation::new(vec![
upsert("dotted", EntryKind::Dir, file_attrs(0, 1)),
Op::Upsert { path: dotted, kind: EntryKind::File, attrs: file_attrs(10, 2) },
upsert("repeated", EntryKind::Dir, file_attrs(0, 3)),
Op::Upsert { path: repeated, kind: EntryKind::File, attrs: file_attrs(20, 4) },
]));
let commit = outcome.commit.as_ref().expect("one exact commit");
for (actual, canonical) in [
(commit.changes[1].path(), dotted_canonical.as_path()),
(commit.changes[3].path(), repeated_canonical.as_path()),
] {
assert_eq!(
actual.as_os_str().as_encoded_bytes(),
canonical.as_os_str().as_encoded_bytes()
);
}
for canonical in [&dotted_canonical, &repeated_canonical] {
let dirty = commit
.impact
.dirty_paths
.iter()
.find(|candidate| candidate.as_path() == canonical)
.expect("changed path is dirty");
assert_eq!(
dirty.as_os_str().as_encoded_bytes(),
canonical.as_os_str().as_encoded_bytes()
);
}
}
#[test]
fn trailing_path_spellings_leave_errors_and_changes_canonical() {
let separator = std::path::MAIN_SEPARATOR;
let canonical = format!("a{separator}b");
for spelling in [format!("a{separator}b{separator}"), format!("a{separator}b{separator}.")]
{
let file = |mtime_ns| Op::Upsert {
path: PathBuf::from(&spelling),
kind: EntryKind::File,
attrs: file_attrs(1, mtime_ns),
};
let mut index = Index::new("/root");
let error = index
.apply(&Observation::new(vec![file(1)]))
.expect_err("a child of an unknown directory is refused");
let crate::Error::UnknownAncestry { path, .. } = error else {
panic!("expected unknown ancestry for {spelling:?}, got {error}");
};
assert_eq!(path.as_os_str().as_encoded_bytes(), canonical.as_bytes(), "{spelling:?}");
let outcome = index.apply_ok(&Observation::new(vec![
upsert("a", EntryKind::Dir, file_attrs(0, 1)),
file(2),
]));
let commit = outcome.commit.as_ref().expect("one exact commit");
assert_eq!(
commit.changes[1].path().as_os_str().as_encoded_bytes(),
canonical.as_bytes(),
"{spelling:?}"
);
}
}
#[test]
fn malformed_batch_is_rejected_before_any_index_mutation() {
let invalid_paths = [
PathBuf::from("../escape"),
PathBuf::from(format!("{}absolute", std::path::MAIN_SEPARATOR)),
];
for invalid_path in invalid_paths {
for invalid_first in [false, true] {
let mut index = index_with_sample_tree();
let before_clock = index.clock;
let before_live = index.live;
let before_total = index.total();
let before_journal = index.journal.clone();
let before_journal_cost = index.journal_cost;
let before_journal_floor = index.journal_floor;
let before_invalidations = index.pending_invalidations.clone();
let before_freshness_epoch = index.freshness_epoch;
let before_freshness = index.freshness();
let valid = upsert("new.txt", EntryKind::File, file_attrs(99, 99));
let invalid = Op::InvalidateSubtree {
path: invalid_path.clone(),
reason: InvalidateReason::Requested,
};
let ops = if invalid_first { vec![invalid, valid] } else { vec![valid, invalid] };
let error = index.apply(&Observation::new(ops)).expect_err("malformed batch");
assert!(
matches!(error, crate::Error::PathEscapesRoot(path) if path == invalid_path)
);
assert_eq!(index.clock, before_clock);
assert_eq!(index.live, before_live);
assert_eq!(index.total(), before_total);
assert_eq!(index.journal, before_journal);
assert_eq!(index.journal_cost, before_journal_cost);
assert_eq!(index.journal_floor, before_journal_floor);
assert_eq!(index.pending_invalidations, before_invalidations);
assert_eq!(index.freshness_epoch, before_freshness_epoch);
assert_eq!(index.freshness(), before_freshness);
assert!(index.lookup(Path::new("new.txt")).is_none());
}
}
}
#[cfg(windows)]
#[test]
fn windows_prefix_is_rejected_before_mutation() {
let mut index = index_with_sample_tree();
let before_clock = index.clock();
let error = index
.apply(&Observation::new(vec![Op::Remove { path: PathBuf::from(r"C:\escape") }]))
.expect_err("prefixed path");
assert!(
matches!(error, crate::Error::PathEscapesRoot(path) if path == Path::new(r"C:\escape"))
);
assert_eq!(index.clock(), before_clock);
}
fn index_with_sample_tree() -> Index {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("src", EntryKind::Dir, Attrs::default()),
upsert("src/main.rs", EntryKind::File, file_attrs(100, 10)),
upsert("src/lib.rs", EntryKind::File, file_attrs(200, 20)),
upsert("docs", EntryKind::Dir, Attrs::default()),
upsert("docs/guide.md", EntryKind::File, file_attrs(300, 30)),
]));
index
}
#[test]
fn the_extension_interner_reclaims_ids_after_churn() {
let mut index = Index::new("/root");
for sequence in 0..128 {
let path = PathBuf::from(format!("build.out-{sequence}"));
index.apply_ok(&Observation::new(vec![Op::Upsert {
path: path.clone(),
kind: EntryKind::File,
attrs: file_attrs(1, sequence),
}]));
index.apply_ok(&Observation::new(vec![Op::Remove { path }]));
}
assert!(index.ext_ids.is_empty(), "no extension survives the file that named it");
assert_eq!(index.ext_names.len(), 1, "128 dead extensions reuse one interner slot");
assert!(index.total().by_ext.is_empty());
}
#[test]
fn a_reclaimed_extension_id_does_not_alias_a_live_tally() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("keep.rs", EntryKind::File, file_attrs(10, 1)),
upsert("drop.tmp", EntryKind::File, file_attrs(20, 2)),
]));
let retained = index.total();
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("drop.tmp") }]));
index.apply_ok(&Observation::new(vec![upsert(
"next.bak",
EntryKind::File,
file_attrs(30, 3),
)]));
let tallies = index.total().by_ext;
assert_eq!(tallies[".rs"], ExtTally { files: 1, bytes: 10, allocated: 512 });
assert_eq!(tallies[".bak"], ExtTally { files: 1, bytes: 30, allocated: 512 });
assert!(!tallies.contains_key(".tmp"), "the removed extension is gone");
assert_eq!(
retained.by_ext[".tmp"],
ExtTally { files: 1, bytes: 20, allocated: 512 },
"an owned roll-up stays self-describing after its interner slot is reused"
);
assert!(!retained.by_ext.contains_key(".bak"));
}
#[test]
fn rollups_aggregate_up_the_tree() {
let index = index_with_sample_tree();
let total = index.total();
assert_eq!(total.files, 3);
assert_eq!(total.dirs, 2);
assert_eq!(total.bytes, 600);
assert_eq!(total.newest_mtime_ns, 30);
let src = index.rollup(Path::new("src")).expect("src is a directory");
assert_eq!(src.files, 2);
assert_eq!(src.dirs, 0);
assert_eq!(src.bytes, 300);
assert_eq!(src.newest_mtime_ns, 20);
}
#[test]
fn rollups_conserve_hand_counted_populations_through_updates_and_subtree_replacement() {
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
let attrs =
|size, allocated, inode| Attrs { size, allocated, inode, dev: 1, ..Attrs::default() };
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, attrs(6, 512, 1)),
upsert("a", EntryKind::Dir, Attrs::default()),
upsert("a/keep.rs", EntryKind::File, attrs(7, 512, 99)),
upsert("a/drop.log", EntryKind::File, attrs(11, 1_024, 3)),
upsert("z.rs", EntryKind::File, attrs(5, 4_096, 99)),
Op::ControlUpsert { path: PathBuf::from(".gitignore"), source: b"*.log\n".to_vec() },
]));
let total = index.partition_total().expect("control state observed");
assert_eq!(
(total.all.files, total.all.dirs, total.all.bytes, total.all.allocated),
(4, 1, 29, 6_144)
);
assert_eq!(
(total.unignored.files, total.unignored.bytes, total.unignored.allocated),
(3, 18, 5_120)
);
assert_eq!(index.rollup(Path::new("a")).expect("directory").bytes, 18);
assert_eq!(
total.all.by_ext[".rs"].files, 2,
"two paths with one inode each contribute a file"
);
index.apply_ok(&Observation::new(vec![upsert(
"a/drop.log",
EntryKind::File,
attrs(13, 1_536, 3),
)]));
let total = index.partition_total().expect("control state observed");
assert_eq!((total.all.files, total.all.bytes, total.all.allocated), (4, 31, 6_656));
assert_eq!(
(total.unignored.files, total.unignored.bytes, total.unignored.allocated),
(3, 18, 5_120)
);
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("a") }]));
let total = index.partition_total().expect("control state observed");
assert_eq!(
(total.all.files, total.all.dirs, total.all.bytes, total.all.allocated),
(2, 0, 11, 4_608)
);
assert_eq!(total.all, total.unignored);
index.apply_ok(&Observation::new(vec![
upsert("a", EntryKind::Dir, Attrs::default()),
upsert("a/final.log", EntryKind::File, attrs(17, 4_096, 4)),
]));
let total = index.partition_total().expect("control state observed");
assert_eq!(
(total.all.files, total.all.dirs, total.all.bytes, total.all.allocated),
(3, 1, 28, 8_704)
);
assert_eq!(
(
total.unignored.files,
total.unignored.dirs,
total.unignored.bytes,
total.unignored.allocated
),
(2, 1, 11, 4_608)
);
assert_eq!(index.rollup(Path::new("a")).expect("directory").bytes, 17);
}
#[test]
fn symlinks_and_special_nodes_do_not_contribute_regular_file_tallies() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("regular.txt", EntryKind::File, file_attrs(10, 10)),
upsert("link.rs", EntryKind::Symlink, file_attrs(99, 99)),
upsert("socket.md", EntryKind::Other, file_attrs(88, 88)),
]));
let total = index.total();
assert_eq!(total.files, 1);
assert_eq!(total.bytes, 10);
assert_eq!(total.allocated, 512);
assert_eq!(total.newest_mtime_ns, 10);
assert_eq!(total.by_ext[".txt"], ExtTally { files: 1, bytes: 10, allocated: 512 });
assert!(!total.by_ext.contains_key(".rs"));
assert!(!total.by_ext.contains_key(".md"));
index.apply_ok(&Observation::new(vec![upsert(
"link.rs",
EntryKind::File,
file_attrs(99, 99),
)]));
assert_eq!(index.total().files, 2);
assert_eq!(index.total().bytes, 109);
index.apply_ok(&Observation::new(vec![upsert(
"link.rs",
EntryKind::Symlink,
file_attrs(99, 99),
)]));
assert_eq!(index.total().files, 1);
assert_eq!(index.total().bytes, 10);
}
#[test]
fn per_extension_tallies_roll_up_hierarchically() {
let index = index_with_sample_tree();
let total = index.total();
assert_eq!(total.by_ext[".rs"], ExtTally { files: 2, bytes: 300, allocated: 1024 });
assert_eq!(total.by_ext[".md"], ExtTally { files: 1, bytes: 300, allocated: 512 });
let src = index.rollup(Path::new("src")).expect("src is a directory");
assert_eq!(src.by_ext[".rs"], ExtTally { files: 2, bytes: 300, allocated: 1024 });
assert!(!src.by_ext.contains_key(".md"));
}
#[test]
fn per_extension_allocated_tracks_apparent_bytes_separately() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("a.rs", EntryKind::File, file_attrs(1, 10)),
upsert("b.rs", EntryKind::File, file_attrs(2, 20)),
]));
let rs = index.total().by_ext[".rs"];
assert_eq!((rs.files, rs.bytes), (2, 3));
assert_eq!(rs.allocated, 1024, "each file occupies one 512-byte block");
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("b.rs") }]));
let rs = index.total().by_ext[".rs"];
assert_eq!((rs.files, rs.bytes, rs.allocated), (1, 1, 512));
}
#[test]
fn upsert_with_matching_fingerprint_is_a_no_op() {
let mut index = index_with_sample_tree();
let before = index.total();
let mark = index.clock();
let stats = index.apply_ok(&Observation::new(vec![upsert(
"src/main.rs",
EntryKind::File,
file_attrs(100, 10),
)]));
assert_eq!(stats.unchanged, 1);
assert_eq!(stats.updated, 0);
assert_eq!(index.total(), before);
assert_eq!(index.clock(), mark);
assert!(index.since(mark).commits.is_empty());
}
#[test]
fn commit_contains_only_effective_mutations() {
let mut index = index_with_sample_tree();
let outcome = index.apply_ok(&Observation::new(vec![
upsert("src/main.rs", EntryKind::File, file_attrs(100, 10)),
upsert("new.txt", EntryKind::File, file_attrs(4, 4)),
Op::Remove { path: PathBuf::from("missing.txt") },
]));
assert_eq!(outcome.stats.unchanged, 2);
let commit = outcome.commit.expect("one effective insert");
assert_eq!(commit.changes.len(), 1);
assert_eq!(commit.changes[0].path(), Path::new("new.txt"));
}
#[test]
fn mutation_counters_match_exact_batch_and_consequence_totals() {
struct DisableCounters;
impl Drop for DisableCounters {
fn drop(&mut self) {
crate::counters::enable(false);
crate::counters::test_thread_reset();
}
}
let _serial = crate::counters::test_serial();
crate::counters::enable(true);
let _disable = DisableCounters;
let observation = Observation::new(vec![
upsert("a", EntryKind::Dir, Attrs::default()),
upsert("a/f1", EntryKind::File, file_attrs(1, 1)),
upsert("a/f2", EntryKind::File, file_attrs(2, 2)),
]);
crate::counters::test_thread_reset();
let mut baseline = Index::new("/root");
baseline.apply_baseline_ok(&observation);
let counts = crate::counters::test_thread_snapshot();
assert_eq!(counts.baseline_batches, 1);
assert_eq!(counts.baseline_accepted_ops, 3);
assert_eq!(counts.opened_batches, 0);
assert_eq!(counts.public_batches, 0);
assert_eq!(counts.ancestry_overlay_inserts, 3);
assert_eq!(counts.ancestry_path_comparisons, 2);
assert_eq!(counts.ancestry_parent_proofs, 3);
assert_eq!(counts.effect_paths, 0);
assert_eq!(counts.effect_path_bytes, 0);
assert_eq!(counts.impact_candidates, 0);
assert_eq!(counts.impact_ancestor_visits, 0);
assert_eq!(counts.impact_retained_dirty_paths, 0);
assert_eq!(counts.impact_all_dirty, 0);
assert_eq!(counts.journal_cloned_commits, 0);
assert_eq!(counts.journal_retained_commits, 0);
crate::counters::test_thread_reset();
let mut public = Index::new("/root");
public.apply_ok(&observation);
let counts = crate::counters::test_thread_snapshot();
assert_eq!(counts.baseline_batches, 0);
assert_eq!(counts.opened_batches, 0);
assert_eq!(counts.public_batches, 1);
assert_eq!(counts.public_accepted_ops, 3);
assert_eq!(counts.effect_paths, 3);
assert_eq!(counts.journal_cloned_commits, 1);
assert_eq!(counts.journal_retained_commits, 1);
assert_eq!(counts.journal_oversized_commits, 0);
assert_eq!(counts.journal_dropped_commits, 0);
crate::counters::test_thread_reset();
let opened = IndexHandle::new(Index::new("/root"));
opened
.apply_discovery(&observation, DiscoveryCommit::default())
.expect("opened discovery batch");
let counts = crate::counters::test_thread_snapshot();
assert_eq!(counts.baseline_batches, 0);
assert_eq!(counts.opened_batches, 1);
assert_eq!(counts.opened_accepted_ops, 3);
assert_eq!(counts.public_batches, 0);
crate::counters::test_thread_reset();
let mut scanner = Index::new("/root");
scanner
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![
upsert("a", EntryKind::Dir, Attrs::default()),
upsert("a/f1", EntryKind::File, file_attrs(1, 1)),
upsert("a/f2", EntryKind::File, file_attrs(2, 2)),
]))
.expect("private scanner batch");
let counts = crate::counters::test_thread_snapshot();
assert_eq!(counts.baseline_batches, 1);
assert_eq!(counts.baseline_accepted_ops, 3);
assert_eq!(counts.ancestry_overlay_inserts, 0);
assert_eq!(counts.ancestry_path_comparisons, 2);
assert_eq!(counts.ancestry_parent_proofs, 3);
assert_eq!(counts.parent_resolutions, 0);
assert_eq!(counts.parent_memo_hits, 0);
assert_eq!(scanner.total().dirs, 1);
assert_eq!(scanner.total().files, 2);
crate::counters::test_thread_reset();
let opened_scanner = IndexHandle::new(Index::new("/root"));
opened_scanner
.apply_scanner_discovery_bounded(
crate::scan::ScannerBatch::from_ops(vec![
upsert("a", EntryKind::Dir, Attrs::default()),
upsert("a/f1", EntryKind::File, file_attrs(1, 1)),
upsert("a/f2", EntryKind::File, file_attrs(2, 2)),
]),
DiscoveryCommit::default(),
None,
)
.expect("opened scanner batch");
let counts = crate::counters::test_thread_snapshot();
assert_eq!(counts.opened_batches, 1);
assert_eq!(counts.opened_accepted_ops, 3);
assert_eq!(counts.ancestry_overlay_inserts, 0);
assert_eq!(counts.effect_paths, 3);
assert_eq!(counts.journal_retained_commits, 1);
let two_commits = 2 * commit_cost(vec![upsert("one", EntryKind::File, file_attrs(1, 1))]);
crate::counters::test_thread_reset();
let mut bounded = Index::new("/root");
bounded.journal_capacity_bytes = two_commits;
bounded.apply_ok(&Observation::new(vec![upsert("one", EntryKind::File, file_attrs(1, 1))]));
bounded.apply_ok(&Observation::new(vec![upsert("two", EntryKind::File, file_attrs(2, 2))]));
bounded.journal_capacity_bytes = 1;
bounded.apply_ok(&Observation::new(vec![upsert(
"three",
EntryKind::File,
file_attrs(3, 3),
)]));
let counts = crate::counters::test_thread_snapshot();
assert_eq!(counts.journal_cloned_commits, 2);
assert_eq!(counts.journal_retained_commits, 2);
assert_eq!(counts.journal_oversized_commits, 1);
assert_eq!(counts.journal_dropped_commits, 2);
}
#[test]
fn detached_and_exact_reducers_produce_the_same_facts_and_stats() {
fn fact_image(index: &Index) -> Vec<(PathBuf, EntryKind, Attrs, bool, Source, bool)> {
let mut image = Vec::new();
let mut frontier = VecDeque::from([EntryId::ROOT]);
while let Some(id) = frontier.pop_front() {
let entry = index.entry(id);
if entry.kind.is_dir() {
frontier.extend(index.child_ids(id));
}
image.push((
index.path_of(id).expect("live entry path"),
entry.kind,
entry.attrs,
entry.ignored,
entry.source,
entry.directory.as_deref().is_none_or(|directory| directory.children_complete),
));
}
image.sort_by(|left, right| left.0.cmp(&right.0));
image
}
fn assert_same_facts(detached: &Index, exact: &Index) {
assert_eq!(fact_image(detached), fact_image(exact));
assert_eq!(detached.total(), exact.total());
let partitions =
|index: &Index| index.named_partitions(index.entry(EntryId::ROOT).rollup());
assert_eq!(partitions(detached), partitions(exact));
let controls = |index: &Index| {
index
.controls
.sources()
.map(|(path, source)| (path, source.to_vec()))
.collect::<Vec<_>>()
};
assert_eq!(controls(detached), controls(exact));
assert_eq!(detached.state, exact.state);
assert_eq!(detached.issues, exact.issues);
let freshness = |index: &Index| {
index
.freshness_marks
.iter()
.map(|(path, mark)| (path.clone(), mark.state, mark.epoch))
.collect::<Vec<_>>()
};
assert_eq!(freshness(detached), freshness(exact));
assert_eq!(detached.verified, exact.verified);
assert_eq!(detached.pending_invalidations, exact.pending_invalidations);
}
let mut detached = Index::new("/root");
let mut exact = detached.clone();
let batches = [
Observation::new(vec![
upsert("a", EntryKind::Dir, file_attrs(0, 1)),
upsert("a/one.rs", EntryKind::File, file_attrs(10, 2)),
upsert("a/two.txt", EntryKind::File, file_attrs(20, 3)),
]),
Observation::new(vec![
upsert("a/one.rs", EntryKind::File, file_attrs(30, 4)),
Op::Remove { path: PathBuf::from("a/two.txt") },
Op::InvalidateSubtree {
path: PathBuf::from("a"),
reason: InvalidateReason::VerificationFailed,
},
]),
];
for batch in batches {
let detached_stats = detached.apply_baseline(&batch).expect("detached batch");
let exact_outcome = exact.apply(&batch).expect("exact batch");
assert_eq!(detached_stats, exact_outcome.stats);
exact.establish_baseline();
assert_same_facts(&detached, &exact);
}
}
#[test]
fn exact_commit_records_verified_ancestry_and_kind_replacement() {
let mut index = Index::new("/root");
let inserted = index.apply_ok(&Observation::new(vec![
upsert("unknown", EntryKind::Dir, file_attrs(0, 1)),
upsert("unknown/deep", EntryKind::Dir, file_attrs(0, 2)),
upsert("unknown/deep/file.txt", EntryKind::File, file_attrs(10, 3)),
]));
let inserted = inserted.commit.expect("ancestry commit");
assert_eq!(
inserted.changes.iter().map(EffectiveChange::path).collect::<Vec<_>>(),
[Path::new("unknown"), Path::new("unknown/deep"), Path::new("unknown/deep/file.txt")]
);
assert!(
inserted
.changes
.iter()
.all(|change| matches!(change, EffectiveChange::Inserted { .. }))
);
let replaced = index.apply_ok(&Observation::new(vec![upsert(
"unknown",
EntryKind::File,
file_attrs(20, 2),
)]));
let replaced = replaced.commit.expect("replacement commit");
assert_eq!(
replaced.changes,
vec![
EffectiveChange::Removed {
path: "unknown".into(),
kind: EntryKind::Dir,
attrs: file_attrs(0, 1),
},
EffectiveChange::Removed {
path: "unknown/deep".into(),
kind: EntryKind::Dir,
attrs: file_attrs(0, 2),
},
EffectiveChange::Removed {
path: "unknown/deep/file.txt".into(),
kind: EntryKind::File,
attrs: file_attrs(10, 3),
},
EffectiveChange::Inserted {
path: "unknown".into(),
kind: EntryKind::File,
attrs: file_attrs(20, 2),
},
]
);
assert_eq!(
replaced.impact.domains,
vec![
ImpactDomain::Topology,
ImpactDomain::Metadata,
ImpactDomain::Classification,
ImpactDomain::Aggregates,
ImpactDomain::Content,
]
);
assert_eq!(
replaced.impact.dirty_paths,
vec![
PathBuf::new(),
"unknown".into(),
"unknown/deep".into(),
"unknown/deep/file.txt".into(),
]
);
}
#[test]
fn rejected_prepared_commit_is_fault_atomic() {
let mut index = index_with_sample_tree();
let before_clock = index.clock();
let before_total = index.total();
let before_len = index.len();
let before_history = index.since(Clock::ZERO);
let mut prepared = prepare_observation(&Observation::new(vec![upsert(
"new/deep.txt",
EntryKind::File,
file_attrs(99, 99),
)]))
.expect("valid preparation");
prepared.reject_before_apply = true;
let error = index.commit_prepared(prepared, true).expect_err("injected preflight");
assert!(matches!(error, crate::Error::CommitRejected("injected reducer preflight")));
assert_eq!(index.clock(), before_clock);
assert_eq!(index.total(), before_total);
assert_eq!(index.len(), before_len);
assert_eq!(index.since(Clock::ZERO), before_history);
assert!(index.lookup(Path::new("new")).is_none());
}
#[test]
fn reconciliation_state_moves_through_exact_commits() {
let mut index = Index::new("/root");
let (started, start) = index.begin_reconcile(Path::new("src")).expect("begin");
let start = start.expect("start commit");
assert!(start.changes.is_empty());
assert_eq!(
start.state,
vec![
StateTransition::Freshness {
path: "src".into(),
previous: Freshness::Fresh,
current: Freshness::Reconciling,
},
StateTransition::IndexState {
previous: IndexState::default(),
current: IndexState {
freshness: Freshness::Reconciling,
..IndexState::default()
},
},
]
);
let finish = index
.finish_reconcile(
Path::new("src"),
started,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish")
.commit
.expect("finish commit");
assert!(finish.changes.is_empty());
assert_eq!(
finish.state,
vec![
StateTransition::Verified { path: "src".into() },
StateTransition::Freshness {
path: "src".into(),
previous: Freshness::Reconciling,
current: Freshness::Fresh,
},
StateTransition::IndexState {
previous: IndexState {
freshness: Freshness::Reconciling,
..IndexState::default()
},
current: IndexState::default(),
},
]
);
}
#[cfg(feature = "watch")]
#[test]
fn observation_failure_cannot_overwrite_a_terminal_resource_stop() {
let handle = IndexHandle::new(Index::new("/root"));
handle
.transition_discovery(DiscoveryTransition::BudgetRefused(Issue::resource_budget(0)))
.expect("stop for budget");
let stopped = handle.state().expect("stopped state");
let clock = handle.clock().expect("stopped clock");
let outcome = handle
.transition_observation(ObservationTransition::Failed(Issue::from_error(
&crate::Error::WatchStopped,
)))
.expect("late observer failure is ignored");
assert_eq!(outcome.commit, None);
assert_eq!(handle.state().expect("terminal state"), stopped);
assert_eq!(handle.clock().expect("terminal clock"), clock);
}
#[test]
fn a_terminal_root_refuses_every_discovery_commit() {
let terminals = [
DiscoveryTransition::BudgetRefused(Issue::resource_budget(1)),
DiscoveryTransition::Failed(Issue::from_error(&crate::Error::OpenedIndexClosed)),
];
for terminal in terminals {
let handle = IndexHandle::new(Index::new("/root"));
handle.transition_discovery(DiscoveryTransition::Begin).expect("begin");
handle
.apply_discovery(
&Observation::new(vec![upsert("dir", EntryKind::Dir, Attrs::default())]),
DiscoveryCommit::default(),
)
.expect("listing before the stop");
handle.transition_discovery(terminal.clone()).expect("terminal transition");
let state = handle.state().expect("terminal state");
let clock = handle.clock().expect("terminal clock");
let late = [
(
vec![upsert("dir/late.txt", EntryKind::File, file_attrs(1, 1))],
DiscoveryCommit {
directory_complete: Some(PathBuf::from("dir")),
transition: None,
},
),
(
Vec::new(),
DiscoveryCommit {
directory_complete: None,
transition: Some(DiscoveryTransition::Finish),
},
),
(
Vec::new(),
DiscoveryCommit {
directory_complete: None,
transition: Some(DiscoveryTransition::Inaccessible {
issues: vec![Issue::resource_budget(2)],
omitted: 0,
}),
},
),
];
for (ops, discovery) in late {
assert!(
matches!(
handle.apply_discovery(&Observation::new(ops), discovery.clone()),
Err(crate::Error::OpenedIndexStopped)
),
"after {terminal:?}, {discovery:?} was accepted"
);
}
assert_eq!(handle.state().expect("state"), state, "after {terminal:?}");
assert_eq!(handle.clock().expect("clock"), clock, "after {terminal:?}");
assert_eq!(handle.kind(Path::new("dir/late.txt")).expect("lookup"), None);
}
}
#[test]
fn replaying_the_same_delta_twice_changes_nothing() {
let mut index = Index::new("/root");
let delta = Observation::new(vec![
upsert("a", EntryKind::Dir, Attrs::default()),
upsert("a/f.txt", EntryKind::File, file_attrs(10, 1)),
]);
index.apply_ok(&delta);
let after_first = index.total();
let stats = index.apply_ok(&delta);
assert_eq!(stats.unchanged, 2);
assert_eq!(index.total(), after_first);
assert_eq!(index.len(), 3);
}
#[test]
fn changed_size_updates_every_ancestor() {
let mut index = index_with_sample_tree();
index.apply_ok(&Observation::new(vec![upsert(
"src/main.rs",
EntryKind::File,
file_attrs(150, 11),
)]));
assert_eq!(index.rollup(Path::new("src")).expect("dir").bytes, 350);
assert_eq!(index.total().bytes, 650);
assert_eq!(index.total().by_ext[".rs"], ExtTally { files: 2, bytes: 350, allocated: 1024 });
}
#[test]
fn allocated_size_change_updates_rollups_even_when_fingerprint_matches() {
let mut index = Index::new("/root");
let original = Attrs { allocated: 512, ..file_attrs(100, 10) };
index.apply_ok(&Observation::new(vec![upsert("file.bin", EntryKind::File, original)]));
let repacked = Attrs { allocated: 4096, ..original };
let outcome =
index.apply_ok(&Observation::new(vec![upsert("file.bin", EntryKind::File, repacked)]));
assert_eq!(outcome.stats.updated, 1);
assert_eq!(outcome.stats.unchanged, 0);
assert_eq!(index.total().allocated, 4096);
assert_eq!(index.attrs(Path::new("file.bin")), Some(&repacked));
}
#[test]
fn newest_mtime_preserves_pre_epoch_values_through_updates_and_removals() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("newer.txt", EntryKind::File, file_attrs(10, -10)),
upsert("older.txt", EntryKind::File, file_attrs(20, -20)),
]));
assert_eq!(index.total().newest_mtime_ns, -10);
index.apply_ok(&Observation::new(vec![upsert(
"newer.txt",
EntryKind::File,
file_attrs(10, -30),
)]));
assert_eq!(index.total().newest_mtime_ns, -20);
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("older.txt") }]));
assert_eq!(index.total().newest_mtime_ns, -30);
}
#[test]
fn removing_a_file_corrects_sums_and_rebuilds_the_max() {
let mut index = index_with_sample_tree();
let stats = index
.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("docs/guide.md") }]));
assert_eq!(stats.removed, 1);
let total = index.total();
assert_eq!(total.files, 2);
assert_eq!(total.bytes, 300);
assert_eq!(total.newest_mtime_ns, 20, "max must fall back to src/lib.rs");
assert!(!total.by_ext.contains_key(".md"), "emptied tallies are dropped");
}
#[test]
fn removing_a_directory_cascades_to_descendants() {
let mut index = index_with_sample_tree();
let stats = index
.apply(&Observation::new(vec![Op::Remove { path: PathBuf::from("src") }]))
.expect("valid observation");
assert_eq!(stats.removed, 3, "the directory and both files");
let total = index.total();
assert_eq!(total.files, 1);
assert_eq!(total.dirs, 1);
assert_eq!(total.bytes, 300);
assert!(index.lookup(Path::new("src/main.rs")).is_none());
assert!(!total.by_ext.contains_key(".rs"));
}
#[test]
fn freed_slots_are_reused() {
let mut index = index_with_sample_tree();
let before = index.len();
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("src") }]));
index.apply_ok(&Observation::new(vec![
upsert("other", EntryKind::Dir, Attrs::default()),
upsert("other/x.rs", EntryKind::File, file_attrs(1, 1)),
upsert("other/y.rs", EntryKind::File, file_attrs(1, 1)),
]));
assert_eq!(index.len(), before, "three freed slots, three new entries");
}
#[test]
fn stale_entry_handle_does_not_alias_a_reused_slot() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"first.txt",
EntryKind::File,
file_attrs(1, 1),
)]));
let stale = index.lookup(Path::new("first.txt")).expect("first id");
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("first.txt") }]));
index.apply_ok(&Observation::new(vec![upsert(
"second.txt",
EntryKind::File,
file_attrs(2, 2),
)]));
let current = index.lookup(Path::new("second.txt")).expect("second id");
assert_ne!(stale, current, "generation participates in handle identity");
assert!(index.attrs_of(stale).is_none());
assert!(index.kind_of(stale).is_none());
assert!(index.name_of(stale).is_none());
assert!(index.path_of(stale).is_none());
assert!(index.children_of(stale).is_none());
assert!(index.rollup_of(stale).is_none());
assert_eq!(index.attrs_of(current).map(|attrs| attrs.size), Some(2));
}
#[test]
fn parent_first_batch_establishes_exact_ancestry() {
let mut index = Index::new("/root");
let deep = file_attrs(0, 1);
let nested = file_attrs(0, 2);
let tree = file_attrs(0, 3);
index.apply_ok(&Observation::new(vec![
upsert("deep", EntryKind::Dir, deep),
upsert("deep/nested", EntryKind::Dir, nested),
upsert("deep/nested/tree", EntryKind::Dir, tree),
upsert("deep/nested/tree/file.txt", EntryKind::File, file_attrs(42, 7)),
]));
assert_eq!(index.total().files, 1);
assert_eq!(index.total().dirs, 3);
assert_eq!(index.total().bytes, 42);
assert_eq!(index.rollup(Path::new("deep/nested")).expect("created").files, 1);
assert_eq!(index.attrs(Path::new("deep")), Some(&deep));
assert_eq!(index.attrs(Path::new("deep/nested")), Some(&nested));
assert_eq!(index.attrs(Path::new("deep/nested/tree")), Some(&tree));
}
#[test]
fn scanner_parent_proof_matches_public_parent_first_application() {
let ops = vec![
upsert("deep", EntryKind::Dir, file_attrs(0, 1)),
upsert("deep/nested", EntryKind::Dir, file_attrs(0, 2)),
upsert("deep/nested/file.txt", EntryKind::File, file_attrs(42, 3)),
];
let mut scanner = Index::new("/root");
let scanner_stats = scanner
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(ops.clone()))
.expect("scanner proof");
let mut public = Index::new("/root");
let public_stats = public.apply(&Observation::new(ops)).expect("public proof").stats;
assert_eq!(scanner_stats, public_stats);
assert_eq!(scanner.total(), public.total());
assert_eq!(scanner.len(), public.len());
for path in ["deep", "deep/nested", "deep/nested/file.txt"] {
assert_eq!(scanner.kind(Path::new(path)), public.kind(Path::new(path)));
assert_eq!(scanner.attrs(Path::new(path)), public.attrs(Path::new(path)));
}
}
#[test]
fn scanner_parent_proof_rejects_unknown_ancestry_before_mutation() {
let mut index = Index::new("/root");
let before = index.total();
let error = index
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![upsert(
"missing/child.txt",
EntryKind::File,
file_attrs(1, 1),
)]))
.expect_err("scanner proof must reject an unknown parent");
assert!(matches!(
error,
crate::Error::UnknownAncestry { path, .. }
if path == Path::new("missing/child.txt")
));
assert_eq!(index.total(), before);
assert_eq!(index.len(), 1);
assert_eq!(index.clock(), Clock::ZERO);
}
#[test]
fn scanner_kind_replacement_is_proved_by_the_general_lane() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("a", EntryKind::Dir, file_attrs(0, 1)),
upsert("a/old.txt", EntryKind::File, file_attrs(7, 1)),
]));
let before = index.total();
let before_clock = index.clock();
let error = index
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![
upsert("a", EntryKind::File, file_attrs(2, 2)),
upsert("a/new.txt", EntryKind::File, file_attrs(3, 2)),
]))
.expect_err("a child cannot attach after its parent became a file");
assert!(matches!(
error,
crate::Error::UnknownAncestry { path, .. } if path == Path::new("a/new.txt")
));
assert_eq!(index.total(), before);
assert_eq!(index.clock(), before_clock);
assert_eq!(index.kind(Path::new("a")), Some(EntryKind::Dir));
assert!(index.lookup(Path::new("a/old.txt")).is_some());
assert!(index.lookup(Path::new("a/new.txt")).is_none());
index
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![upsert(
"a",
EntryKind::File,
file_attrs(2, 2),
)]))
.expect("a scanner batch can replace an entry's kind");
assert_eq!(index.kind(Path::new("a")), Some(EntryKind::File));
assert!(index.lookup(Path::new("a/old.txt")).is_none());
let total = index.total();
assert_eq!((total.files, total.dirs, total.bytes), (1, 0, 2));
}
#[test]
fn scanner_discovery_survives_a_kind_changed_by_a_concurrent_refresh() {
let handle = IndexHandle::new(Index::new("/root"));
handle
.apply_scanner_discovery_bounded(
crate::scan::ScannerBatch::from_ops(vec![upsert(
"p",
EntryKind::Dir,
Attrs::default(),
)]),
DiscoveryCommit::default(),
None,
)
.expect("root listing");
handle
.apply(&Observation::new(vec![upsert("p/d", EntryKind::File, file_attrs(1, 1))]))
.expect("refresh insert");
let outcome = handle.apply_scanner_discovery_bounded(
crate::scan::ScannerBatch::from_ops(vec![upsert(
"p/d",
EntryKind::Dir,
Attrs::default(),
)]),
DiscoveryCommit { directory_complete: Some(PathBuf::from("p")), transition: None },
None,
);
assert!(outcome.is_ok(), "discovery must not die on a kind race: {outcome:?}");
assert_eq!(handle.kind(Path::new("p/d")).expect("kind read"), Some(EntryKind::Dir));
assert_eq!(handle.directory_complete(Path::new("p")).expect("read"), Some(true));
}
#[test]
fn live_upsert_refuses_unknown_ancestry_without_mutation() {
let mut index = Index::new("/root");
let before = index.clock();
let error = index
.apply(&Observation::new(vec![upsert(
"unknown/deep/file.txt",
EntryKind::File,
file_attrs(10, 1),
)]))
.expect_err("live input must not invent parent metadata");
assert!(matches!(
error,
crate::Error::UnknownAncestry { path, reconcile_from }
if path == Path::new("unknown/deep/file.txt")
&& reconcile_from.as_os_str().is_empty()
));
assert_eq!(index.clock(), before);
assert_eq!(index.len(), 1);
assert!(index.since(before).commits.is_empty());
}
#[test]
fn explicit_kind_replacement_precedes_attaching_a_child() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"conflict",
EntryKind::File,
file_attrs(9, 1),
)]));
let outcome = index.apply_ok(&Observation::new(vec![
upsert("conflict", EntryKind::Dir, file_attrs(0, 2)),
upsert("conflict/child.txt", EntryKind::File, file_attrs(4, 2)),
]));
assert_eq!(index.kind(Path::new("conflict")), Some(EntryKind::Dir));
assert!(index.lookup(Path::new("conflict/child.txt")).is_some());
assert_eq!(index.total().files, 1);
assert_eq!(index.total().dirs, 1);
assert_eq!(index.total().bytes, 4);
assert_eq!(outcome.removed, 1);
}
#[test]
fn kind_change_replaces_the_entry() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert(
"thing",
EntryKind::File,
file_attrs(50, 5),
)]));
assert_eq!(index.total().files, 1);
index.apply_ok(&Observation::new(vec![upsert("thing", EntryKind::Dir, Attrs::default())]));
let total = index.total();
assert_eq!(total.files, 0);
assert_eq!(total.dirs, 1);
assert_eq!(total.bytes, 0);
}
#[test]
fn paths_are_reconstructed_from_parent_pointers() {
let index = index_with_sample_tree();
let id = index.lookup(Path::new("src/main.rs")).expect("present");
assert_eq!(index.path_of(id), Some(PathBuf::from("src/main.rs")));
assert_eq!(index.path_of(EntryId::ROOT), Some(PathBuf::new()));
}
#[test]
fn since_returns_commits_after_a_clock() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("a.txt", EntryKind::File, file_attrs(1, 1))]));
let mark = index.clock();
index.apply_ok(&Observation::new(vec![upsert("b.txt", EntryKind::File, file_attrs(2, 2))]));
let since = index.since(mark);
assert!(!since.truncated);
assert_eq!(since.commits.len(), 1);
assert_eq!(since.commits[0].changes[0].path(), Path::new("b.txt"));
assert_eq!(index.since(index.clock()).commits.len(), 0);
}
fn commit_cost(ops: Vec<Op>) -> usize {
let mut probe = Index::new("/root");
probe.apply_ok(&Observation::new(ops)).commit.expect("effective commit").retained_cost()
}
#[test]
fn oversized_single_batch_is_not_retained() {
let batch = || {
vec![
upsert("a.txt", EntryKind::File, file_attrs(1, 1)),
upsert("b.txt", EntryKind::File, file_attrs(2, 2)),
upsert("c.txt", EntryKind::File, file_attrs(3, 3)),
]
};
let mut index = Index::with_journal_capacity_bytes("/root", commit_cost(batch()) - 1);
let outcome = index.apply_ok(&Observation::new(batch()));
assert_eq!(outcome.commit.as_ref().expect("committed").changes.len(), 3);
let since = index.since(Clock::ZERO);
assert!(since.truncated);
assert!(since.commits.is_empty());
}
#[test]
fn journal_eviction_charges_the_complete_retained_payload() {
let first = || {
vec![
upsert("a.txt", EntryKind::File, file_attrs(1, 1)),
upsert("b.txt", EntryKind::File, file_attrs(2, 2)),
]
};
let second = || {
vec![
upsert("c.txt", EntryKind::File, file_attrs(3, 3)),
upsert("d.txt", EntryKind::File, file_attrs(4, 4)),
]
};
let capacity = commit_cost(first()) + commit_cost(second()) - 1;
let mut index = Index::with_journal_capacity_bytes("/root", capacity);
index.apply_ok(&Observation::new(first()));
index.apply_ok(&Observation::new(second()));
let since = index.since(Clock::ZERO);
assert!(since.truncated);
assert_eq!(since.commits.len(), 1);
assert_eq!(since.commits[0].changes.len(), 2);
assert_eq!(since.commits[0].changes[0].path(), Path::new("c.txt"));
}
#[test]
fn journal_eviction_is_charged_in_path_bytes() {
let long_name = format!("{}.txt", "n".repeat(4096));
let short = || vec![upsert("a.txt", EntryKind::File, file_attrs(1, 1))];
let long = || vec![upsert(&long_name, EntryKind::File, file_attrs(2, 2))];
let short_cost = commit_cost(short());
let long_cost = commit_cost(long());
assert!(
long_cost > short_cost + 4096,
"path bytes must be charged: short {short_cost}, long {long_cost}"
);
let mut index = Index::with_journal_capacity_bytes("/root", short_cost + long_cost - 1);
index.apply_ok(&Observation::new(short()));
index.apply_ok(&Observation::new(long()));
let since = index.since(Clock::ZERO);
assert!(since.truncated, "an item budget kept both commits; a byte budget cannot");
assert_eq!(since.commits.len(), 1);
assert_eq!(since.commits[0].changes[0].path(), Path::new(&long_name));
}
#[test]
fn impact_drops_an_overflowing_path_set_instead_of_truncating_it() {
let mut index = Index::new("/root");
let ops = (0..=MAX_DIRTY_PATHS)
.map(|which| {
upsert(
&format!("file-{which}.txt"),
EntryKind::File,
file_attrs(u64::try_from(which).expect("bounded"), 1),
)
})
.collect();
let commit =
index.apply_ok(&Observation::new(ops)).commit.expect("overflowing impact commit");
assert!(commit.impact.all_dirty);
assert!(commit.impact.dirty_paths.is_empty(), "a partial path list must not escape");
assert_eq!(commit.changes.len(), MAX_DIRTY_PATHS + 1);
}
#[test]
fn invalidations_are_queued_for_the_scan_layer() {
let mut index = Index::new("/root");
let stats = index.apply_ok(&Observation::new(vec![Op::InvalidateSubtree {
path: PathBuf::from("src"),
reason: InvalidateReason::WatchOverflow,
}]));
assert_eq!(stats.invalidated, 1);
let pending = index.take_pending_invalidations();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].0, PathBuf::from("src"));
assert_eq!(pending[0].1, InvalidateReason::WatchOverflow);
assert!(index.take_pending_invalidations().is_empty(), "drained once");
}
#[test]
fn paths_escaping_the_root_are_rejected() {
assert!(normalize(Path::new("../escape")).is_none());
assert!(normalize(Path::new("/absolute")).is_none());
assert_eq!(
normalize(Path::new("./a/b")).expect("relative"),
vec![OsString::from("a"), OsString::from("b")]
);
let mut index = Index::new("/root");
let upsert_error = index
.apply(&Observation::new(vec![upsert("../escape", EntryKind::File, file_attrs(1, 1))]))
.expect_err("escaping upsert");
assert!(matches!(upsert_error, crate::Error::PathEscapesRoot(_)));
assert_eq!(index.total().files, 0);
let invalidation_error = index
.apply(&Observation::new(vec![Op::InvalidateSubtree {
path: PathBuf::from("../outside"),
reason: InvalidateReason::Requested,
}]))
.expect_err("escaping invalidation");
assert!(matches!(invalidation_error, crate::Error::PathEscapesRoot(_)));
assert!(index.take_pending_invalidations().is_empty());
assert_eq!(index.freshness(), Freshness::Fresh);
}
#[cfg(unix)]
#[test]
fn distinct_non_utf8_names_have_distinct_identity() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let first = PathBuf::from(OsString::from_vec(vec![b'n', 0x80]));
let second = PathBuf::from(OsString::from_vec(vec![b'n', 0x81]));
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
Op::Upsert { path: first.clone(), kind: EntryKind::File, attrs: file_attrs(10, 1) },
Op::Upsert { path: second.clone(), kind: EntryKind::File, attrs: file_attrs(20, 2) },
]));
assert_eq!(index.total().files, 2);
assert_eq!(index.total().bytes, 30);
assert!(index.lookup(&first).is_some());
assert!(index.lookup(&second).is_some());
assert_serving_indexes(&index);
}
#[cfg(unix)]
#[test]
fn a_non_utf8_parent_still_lists_its_children() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let directory = PathBuf::from(OsString::from_vec(vec![b'd', 0x80]));
let child = directory.join("child");
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
Op::Upsert { path: directory.clone(), kind: EntryKind::Dir, attrs: Attrs::default() },
Op::Upsert { path: child, kind: EntryKind::File, attrs: file_attrs(1, 1) },
]));
assert_serving_indexes(&index);
assert_eq!(
index.portable_children(&directory).map(|children| children.nondirectories.len()),
Some(1)
);
assert!(
index.portable_entries().keys().any(|portable| portable.as_str() == "d%80/child"),
"a child under a non-utf8 directory is listed at its escaped path"
);
}
#[cfg(unix)]
#[test]
fn non_utf8_stem_keeps_ascii_extension_tally() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let path = PathBuf::from(OsString::from_vec(vec![b'n', 0x80, b'.', b'R', b'S']));
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![Op::Upsert {
path,
kind: EntryKind::File,
attrs: file_attrs(10, 1),
}]));
let tallies = index.total().by_ext;
assert_eq!(
tallies.get(".rs"),
Some(&ExtTally { files: 1, bytes: 10, allocated: file_attrs(10, 1).allocated })
);
}
#[test]
fn restore_candidates_count_visited_files_not_pathbuf_aliases() {
use crate::content::AnalysisSet;
let mut index = Index::new("/root");
let attrs = file_attrs(10, 1);
assert!(
index
.insert_loaded_child(EntryId::ROOT, OsString::from("a"), EntryKind::File, attrs)
.is_some()
);
assert!(
index
.insert_loaded_child(EntryId::ROOT, OsString::from("a/"), EntryKind::File, attrs)
.is_some()
);
let (candidates, visited) =
index.restore_analysis_candidates(AnalysisSet::NONE.with_lines());
assert_eq!(visited, 2, "each visited regular file is a completeness slot");
assert_eq!(candidates.len(), 1, "PathBuf keys merge trailing-separator aliases");
}
#[test]
fn restore_candidates_match_analysis_file_identities() {
use crate::content::AnalysisSet;
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("src", EntryKind::Dir, file_attrs(0, 1)),
upsert("src/nested", EntryKind::Dir, file_attrs(0, 2)),
upsert("src/nested/lib.rs", EntryKind::File, file_attrs(10, 1)),
upsert("README.md", EntryKind::File, file_attrs(20, 2)),
]));
let profile = AnalysisSet::NONE.with_lines();
let live = index.analysis_candidates(profile);
let (restore, visited) = index.restore_analysis_candidates(profile);
assert_eq!(restore.len(), live.len());
assert_eq!(visited, u64::try_from(live.len()).expect("candidate count fits u64"));
assert_eq!(restore.len(), 2);
for candidate in &live {
assert_eq!(
index.path_of(candidate.entry_id).as_deref(),
Some(candidate.relative_path.as_path())
);
let restored = restore.get(&candidate.relative_path).expect("same relative path");
assert_eq!(restored.entry_id, candidate.entry_id);
assert_eq!(restored.revision, candidate.revision);
assert_eq!(restored.attrs.fingerprint(), candidate.attrs.fingerprint());
}
}
#[test]
fn content_results_commit_conditionally_and_metadata_changes_invalidate_them() {
use crate::content::{
AnalysisApplyOutcome, AnalysisRequest, AnalysisSet, AnalyzerOutcome, BasicMetrics,
ContentProvenance, FileAnalysis,
};
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("src", EntryKind::Dir, file_attrs(0, 1)),
upsert("src/lib.rs", EntryKind::File, file_attrs(10, 1)),
]));
let profile = AnalysisSet::NONE.with_lines();
let candidate =
index.analysis_candidates(profile).into_iter().next().expect("file candidate");
let analysis = FileAnalysis {
fingerprint: candidate.attrs.fingerprint(),
bytes: candidate.attrs.size,
detection: candidate.classification.clone().into(),
lines: AnalyzerOutcome::analyzed(BasicMetrics {
physical_lines: 2,
nonblank_lines: 2,
..BasicMetrics::default()
}),
code: None,
words: None,
error: None,
};
let provenance = ContentProvenance::for_request(
AnalysisRequest { profile, ..AnalysisRequest::default() },
crate::classify::type_rule_fingerprint(),
);
let observation = AnalysisObservation {
candidate: candidate.clone(),
profile,
provenance: provenance.clone(),
analysis: analysis.clone(),
};
assert_eq!(
index.apply_analysis(observation.clone()),
AnalysisApplyOutcome::Stale,
"an index prepared for no content identity holds no record"
);
assert!(index.content().is_none());
index.prepare_content_analysis(AnalysisRequest { profile, ..AnalysisRequest::default() });
assert_eq!(index.content_set(), profile);
assert_eq!(index.apply_analysis(observation), AnalysisApplyOutcome::Applied);
assert_eq!(
index
.content_rollup(Path::new(""))
.expect("content root")
.total
.lines
.metrics
.physical_lines,
2
);
index.apply_ok(&Observation::new(vec![upsert(
"src/lib.rs",
EntryKind::File,
file_attrs(20, 2),
)]));
assert!(index.content_rollup(Path::new("")).is_none());
assert_eq!(
index.apply_analysis(AnalysisObservation { candidate, profile, provenance, analysis }),
AnalysisApplyOutcome::Stale
);
}
#[test]
fn operational_content_failures_are_retained_but_retried_until_recovery() {
use crate::content::{
AnalysisApplyOutcome, AnalysisRequest, AnalysisSet, AnalyzerOutcome, BasicMetrics,
ContentProvenance, CoverageReason, FileAnalysis,
};
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("src", EntryKind::Dir, file_attrs(0, 1)),
upsert("src/lib.rs", EntryKind::File, file_attrs(10, 1)),
]));
let profile = AnalysisSet::LINES_ONLY;
let request = AnalysisRequest { profile, ..AnalysisRequest::default() };
index.prepare_content_analysis(request);
let candidate = index.pending_analysis_candidates(request).pop().expect("candidate");
let provenance =
ContentProvenance::for_request(request, crate::classify::type_rule_fingerprint());
let record = |reason, error: &str| FileAnalysis {
fingerprint: candidate.attrs.fingerprint(),
bytes: candidate.attrs.size,
detection: candidate.classification.clone().into(),
lines: AnalyzerOutcome::unavailable(reason),
code: None,
words: None,
error: Some(error.to_owned()),
};
for (reason, error) in [
(CoverageReason::IoError, "read failed"),
(CoverageReason::ChangedDuringRead, "changed during read"),
] {
assert_eq!(
index.apply_analysis(AnalysisObservation {
candidate: candidate.clone(),
profile,
provenance: provenance.clone(),
analysis: record(reason, error),
}),
AnalysisApplyOutcome::Applied
);
let retained = index.content().expect("content").file(Path::new("src/lib.rs"));
assert_eq!(retained.and_then(FileAnalysis::operational_failure), Some(reason));
assert_eq!(
index.pending_analysis_candidates(request).len(),
1,
"an operational failure must remain pending"
);
}
let recovered = FileAnalysis {
fingerprint: candidate.attrs.fingerprint(),
bytes: candidate.attrs.size,
detection: candidate.classification.clone().into(),
lines: AnalyzerOutcome::analyzed(BasicMetrics {
physical_lines: 1,
nonblank_lines: 1,
raw_words: 1,
..BasicMetrics::default()
}),
code: None,
words: None,
error: None,
};
assert_eq!(
index.apply_analysis(AnalysisObservation {
candidate,
profile,
provenance,
analysis: recovered,
}),
AnalysisApplyOutcome::Applied
);
assert!(index.pending_analysis_candidates(request).is_empty());
assert_eq!(
index
.content()
.expect("content")
.file(Path::new("src/lib.rs"))
.and_then(FileAnalysis::operational_failure),
None
);
}
#[test]
fn an_index_that_did_not_observe_controls_refuses_ignore_questions() {
let mut index =
Index::new_with_scope("/root", crate::test_support::not_observing_controls());
assert!(!index.observes_controls());
index.apply_ok(&Observation::new(vec![upsert(
"debug.log",
EntryKind::File,
file_attrs(10, 1),
)]));
for path in ["debug.log", "absent.log"] {
assert!(
matches!(
index.is_ignored(Path::new(path)),
Err(crate::Error::ControlStateNotObserved)
),
"{path}"
);
}
assert!(matches!(index.controls(), Err(crate::Error::ControlStateNotObserved)));
let mut observed =
Index::new_with_scope("/root", crate::test_support::observing_controls());
assert!(observed.observes_controls());
observed.apply_ok(&Observation::new(vec![upsert(
"debug.log",
EntryKind::File,
file_attrs(10, 1),
)]));
assert_eq!(observed.is_ignored(Path::new("debug.log")).ok(), Some(Some(false)));
assert_eq!(observed.is_ignored(Path::new("absent.log")).ok(), Some(None));
assert!(observed.controls().is_ok_and(crate::control::ControlTable::is_empty));
}
#[test]
fn an_index_that_does_not_observe_controls_refuses_control_input() {
let mut index =
Index::new_with_scope("/root", crate::test_support::not_observing_controls());
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(6, 1)),
upsert("debug.log", EntryKind::File, file_attrs(10, 2)),
]));
let stale_baseline = index.expectation(Path::new(".gitignore"));
index.apply_ok(&Observation::new(vec![upsert(
".gitignore",
EntryKind::File,
file_attrs(7, 3),
)]));
let clock = index.clock();
let total = index.total();
let controls = || {
[
Op::ControlUpsert {
path: PathBuf::from(".gitignore"),
source: b"*.log\n".to_vec(),
},
Op::ControlRemove { path: PathBuf::from(".gitignore") },
]
};
for control in controls() {
let batch = Observation::new(vec![
upsert("new.txt", EntryKind::File, file_attrs(1, 4)),
control.clone(),
]);
assert!(
matches!(index.apply(&batch), Err(crate::Error::ControlStateNotObserved)),
"{control:?}"
);
assert!(
matches!(index.apply_baseline(&batch), Err(crate::Error::ControlStateNotObserved)),
"{control:?}"
);
let stale = Observation::from_ops(vec![ObservationOp::if_state(
control.clone(),
stale_baseline,
)]);
assert!(
matches!(index.apply(&stale), Err(crate::Error::ControlStateNotObserved)),
"stale {control:?}"
);
}
assert_eq!(index.clock(), clock, "a refused batch commits nothing");
assert_eq!(index.total(), total);
assert!(index.lookup(Path::new("new.txt")).is_none());
assert!(index.control_table().is_empty());
let mut table = crate::control::ControlTable::default();
table.upsert(Path::new(".gitignore"), b"*.log\n".to_vec()).expect("control source");
assert!(matches!(
index.install_controls(table),
Err(crate::Error::ControlStateNotObserved)
));
assert!(index.control_table().is_empty());
let mut observed =
Index::new_with_scope("/root", crate::test_support::observing_controls());
for control in controls() {
observed
.apply(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(6, 1)),
control,
]))
.expect("an observing index accepts control input");
}
}
#[test]
fn an_index_that_did_not_observe_controls_states_no_partition_or_child_ignore_fact() {
let tree = Observation::new(vec![
upsert("dir", EntryKind::Dir, file_attrs(0, 1)),
upsert("dir/debug.log", EntryKind::File, file_attrs(10, 2)),
]);
let mut unobserved =
Index::new_with_scope("/root", crate::test_support::not_observing_controls());
unobserved.apply_ok(&tree);
assert!(matches!(unobserved.partition_total(), Err(crate::Error::ControlStateNotObserved)));
for path in ["", "dir", "dir/debug.log", "absent"] {
assert!(
matches!(
unobserved.partition_rollup(Path::new(path)),
Err(crate::Error::ControlStateNotObserved)
),
"{path}"
);
assert!(
matches!(
unobserved.partition_rollup_summary(Path::new(path)),
Err(crate::Error::ControlStateNotObserved)
),
"{path}"
);
}
assert_eq!(unobserved.total().files, 1, "the all partition still answers");
let children = IndexHandle::new(unobserved)
.children(Path::new(""))
.expect("children read")
.expect("root directory");
assert_eq!(children.len(), 1);
assert_eq!(children[0].ignored, None);
assert_eq!(children[0].partitions, None);
assert_eq!(children[0].rollup.as_ref().map(|rollup| rollup.files), Some(1));
let mut observed =
Index::new_with_scope("/root", crate::test_support::observing_controls());
observed.apply_ok(&tree);
assert_eq!(observed.partition_total().expect("control state observed").unignored.files, 1);
assert!(
observed.partition_rollup(Path::new("dir")).expect("control state observed").is_some()
);
assert_eq!(
observed
.partition_rollup_summary(Path::new("dir/debug.log"))
.expect("control state observed"),
None,
"a file has no partitions"
);
let children = IndexHandle::new(observed)
.children(Path::new(""))
.expect("children read")
.expect("root directory");
assert_eq!(children[0].ignored, Some(false));
assert_eq!(
children[0].partitions.as_ref().map(|partitions| partitions.unignored.files),
Some(1)
);
}
#[test]
fn control_changes_atomically_move_fixed_partitions_without_changing_all() {
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(6, 1)),
upsert("debug.log", EntryKind::File, file_attrs(10, 2)),
upsert("keep.rs", EntryKind::File, file_attrs(20, 3)),
upsert("docs", EntryKind::Dir, file_attrs(0, 4)),
upsert("docs/other.log", EntryKind::File, file_attrs(30, 5)),
upsert("docs/keep.log", EntryKind::File, file_attrs(40, 6)),
]));
let before = index.partition_total().expect("control state observed");
let outcome = index.apply_ok(&Observation::new(vec![Op::ControlUpsert {
path: PathBuf::from(".gitignore"),
source: b"*.log\n".to_vec(),
}]));
let partitions = index.partition_total().expect("control state observed");
assert_eq!(partitions.all, before.all, "classification never changes all facts");
assert_eq!(partitions.all.files, 5);
assert_eq!(partitions.unignored.files, 2);
assert_eq!(partitions.unignored.bytes, 26);
assert_eq!(outcome.controls, 1);
assert_eq!(outcome.reclassified, 3);
assert_eq!(
index.is_ignored(Path::new("debug.log")).expect("control state observed"),
Some(true)
);
assert_eq!(
index.is_ignored(Path::new("keep.rs")).expect("control state observed"),
Some(false)
);
let commit = outcome.commit.expect("control and classification commit together");
assert!(matches!(
commit.changes.first(),
Some(EffectiveChange::ControlUpdated { path, previous: None, current: Some(_) })
if path == Path::new(".gitignore")
));
assert_eq!(
commit
.changes
.iter()
.filter(|change| matches!(change, EffectiveChange::Reclassified { .. }))
.count(),
3
);
}
#[test]
fn serving_semantics_follow_ignore_reclassification_exactly() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
crate::test_support::observing_controls(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(6, 1)),
upsert("debug.log", EntryKind::File, file_attrs(10, 2)),
upsert("keep.rs", EntryKind::File, file_attrs(20, 3)),
upsert("Makefile", EntryKind::File, file_attrs(30, 4)),
]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![Op::ControlUpsert {
path: PathBuf::from(".gitignore"),
source: b"*.log\nMakefile\n".to_vec(),
}]));
assert_serving_indexes(&index);
index.apply_ok(&Observation::new(vec![Op::ControlRemove {
path: PathBuf::from(".gitignore"),
}]));
assert_serving_indexes(&index);
}
#[test]
fn nested_negation_edit_and_last_control_deletion_reclassify_exactly() {
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
index.apply_ok(&Observation::new(vec![
upsert(".gitignore", EntryKind::File, file_attrs(6, 1)),
upsert("docs", EntryKind::Dir, file_attrs(0, 2)),
upsert("docs/.gitignore", EntryKind::File, file_attrs(10, 3)),
upsert("docs/keep.log", EntryKind::File, file_attrs(40, 4)),
Op::ControlUpsert { path: PathBuf::from(".gitignore"), source: b"*.log\n".to_vec() },
Op::ControlUpsert {
path: PathBuf::from("docs/.gitignore"),
source: b"!keep.log\n".to_vec(),
},
]));
assert_eq!(
index.is_ignored(Path::new("docs/keep.log")).expect("control state observed"),
Some(false)
);
let edited = index.apply_ok(&Observation::new(vec![Op::ControlUpsert {
path: PathBuf::from("docs/.gitignore"),
source: b"# no exception\n".to_vec(),
}]));
assert_eq!(edited.reclassified, 1);
assert_eq!(
index.is_ignored(Path::new("docs/keep.log")).expect("control state observed"),
Some(true)
);
let removed = index
.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from(".gitignore") }]));
assert_eq!(removed.controls, 1, "removing the retained row removes its control state");
assert_eq!(removed.reclassified, 1);
assert_eq!(
index.controls().expect("control state observed").len(),
1,
"the nested control remains"
);
assert_eq!(
index.is_ignored(Path::new("docs/keep.log")).expect("control state observed"),
Some(false)
);
index.apply_ok(&Observation::new(vec![Op::Remove {
path: PathBuf::from("docs/.gitignore"),
}]));
assert!(index.controls().expect("control state observed").is_empty());
assert_eq!(
index.is_ignored(Path::new("docs/keep.log")).expect("control state observed"),
Some(false)
);
}
#[test]
fn replacing_batch_ancestors_prunes_retained_and_transient_controls() {
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
index
.apply(&Observation::new(vec![
upsert("docs", EntryKind::Dir, file_attrs(0, 1)),
upsert("docs/.gitignore", EntryKind::File, file_attrs(6, 2)),
Op::ControlUpsert { path: "docs/.gitignore".into(), source: b"*.log\n".to_vec() },
]))
.expect("retained control");
let outcome = index
.apply(&Observation::new(vec![
upsert("scratch", EntryKind::Dir, file_attrs(0, 3)),
upsert("scratch/.gitignore", EntryKind::File, file_attrs(6, 4)),
Op::ControlUpsert {
path: "scratch/.gitignore".into(),
source: b"*.log\n".to_vec(),
},
upsert("scratch", EntryKind::File, file_attrs(1, 5)),
upsert("scratch", EntryKind::Dir, file_attrs(0, 6)),
upsert("scratch/new.log", EntryKind::File, file_attrs(7, 7)),
Op::Remove { path: "docs".into() },
upsert("docs", EntryKind::Dir, file_attrs(0, 8)),
upsert("docs/new.log", EntryKind::File, file_attrs(9, 9)),
]))
.expect("mixed control and structural batch");
assert!(index.controls().expect("control state observed").is_empty());
assert_eq!(
index.is_ignored(Path::new("scratch/new.log")).expect("control state observed"),
Some(false)
);
assert_eq!(
index.is_ignored(Path::new("docs/new.log")).expect("control state observed"),
Some(false)
);
assert_eq!(outcome.stats.controls, 1, "only the retained control has a net change");
let controls = outcome
.commit
.as_ref()
.expect("one exact commit")
.changes
.iter()
.filter(|change| matches!(change, EffectiveChange::ControlUpdated { .. }))
.collect::<Vec<_>>();
assert!(matches!(
controls.as_slice(),
[EffectiveChange::ControlUpdated { path, previous: Some(_), current: None }]
if path == Path::new("docs/.gitignore")
));
}
#[test]
fn an_over_budget_control_is_refused_while_its_batch_commits() {
let mut index = Index::new_opened_with_scope_types_and_journal_capacity_bytes(
"/root",
crate::test_support::observing_controls(),
crate::classify::TypeRegistry::compiled_shared(),
DEFAULT_JOURNAL_CAPACITY_BYTES,
);
index.apply_ok(&Observation::new(vec![
Op::ControlUpsert { path: PathBuf::from(".gitignore"), source: b"*.log\n".to_vec() },
upsert("debug.log", EntryKind::File, file_attrs(10, 1)),
]));
assert_eq!(index.is_ignored(Path::new("debug.log")).expect("observed"), Some(true));
let mut oversized = crate::control::source_at_test_limit();
oversized.push(b'a');
let outcome = index.apply_ok(&Observation::new(vec![
upsert("ordinary.txt", EntryKind::File, file_attrs(1, 2)),
Op::ControlUpsert { path: PathBuf::from(".gitignore"), source: oversized },
]));
assert!(index.lookup(Path::new("ordinary.txt")).is_some(), "ordinary work commits");
assert_eq!(index.total().files, 2);
assert_eq!(
index.is_ignored(Path::new("debug.log")).expect("observed"),
None,
"the refused replacement leaves classification unknown"
);
let changes = &outcome.commit.as_ref().expect("one commit").changes;
let refused = Some(crate::control::ControlRefusalReason::Budget);
assert!(changes.iter().any(|change| matches!(
change,
EffectiveChange::ControlUpdated { previous: Some(_), current: None, .. }
)));
assert!(changes.iter().any(|change| matches!(
change,
EffectiveChange::ControlRefusalUpdated { previous: None, current, .. }
if *current == refused
)));
let crate::control::ControlCoverage::Observed(coverage) = index.control_coverage() else {
panic!("an observing index reports observed coverage");
};
assert_eq!((coverage.applied, coverage.refused), (0, 1));
assert_eq!(coverage.refusals[0].path, Path::new(".gitignore"));
let lifted = index.apply_ok(&Observation::new(vec![Op::ControlRemove {
path: PathBuf::from(".gitignore"),
}]));
assert!(matches!(
lifted.commit.as_ref().expect("lifting a refusal commits").changes.as_slice(),
[EffectiveChange::ControlRefusalUpdated { previous, current: None, .. }]
if *previous == refused
));
assert_eq!(
index.control_coverage(),
crate::control::ControlCoverage::Observed(crate::control::ControlObservation {
limits: crate::control::ControlLimits::default(),
applied: 0,
rules: 0,
refused: 0,
refusals: Vec::new(),
})
);
}
#[test]
fn a_batch_that_cannot_change_the_control_table_does_not_copy_it() {
let projection_clones = |index: &mut Index, ops: Vec<Op>| {
CONTROL_PROJECTION_CLONES.with(|clones| clones.set(0));
let outcome = index.apply_ok(&Observation::new(ops));
(CONTROL_PROJECTION_CLONES.with(std::cell::Cell::get), outcome)
};
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
let (clones, _) = projection_clones(
&mut index,
vec![
upsert("cold", EntryKind::Dir, file_attrs(0, 1)),
upsert("cold/file.txt", EntryKind::File, file_attrs(1, 1)),
],
);
assert_eq!(clones, 0, "an empty table has nothing a structural batch can change");
let mut over_budget = vec![b'x'; crate::control::DEFAULT_CONTROL_LINE_LIMIT + 1];
over_budget.push(b'\n');
index.apply_ok(&Observation::new(vec![
upsert("keep", EntryKind::Dir, file_attrs(0, 1)),
upsert("keep/file.txt", EntryKind::File, file_attrs(3, 1)),
upsert("vendor", EntryKind::Dir, file_attrs(0, 1)),
Op::ControlUpsert { path: PathBuf::from(".gitignore"), source: b"*.log\n".to_vec() },
Op::ControlUpsert {
path: PathBuf::from("vendor/.gitignore"),
source: over_budget.clone(),
},
]));
let coverage = index.control_coverage();
let (clones, outcome) = projection_clones(
&mut index,
vec![
upsert("keep/file.txt", EntryKind::File, file_attrs(4, 1)),
Op::ControlUpsert {
path: PathBuf::from(".gitignore"),
source: b"*.log\n".to_vec(),
},
Op::ControlUpsert { path: PathBuf::from("vendor/.gitignore"), source: over_budget },
],
);
assert_eq!(clones, 0, "no control op changes anything");
assert_eq!(index.control_coverage(), coverage);
assert!(!outcome.commit.expect("the file's size changed").changes.iter().any(
|change| matches!(
change,
EffectiveChange::ControlUpdated { .. }
| EffectiveChange::ControlRefusalUpdated { .. }
)
));
let (clones, _) = projection_clones(
&mut index,
vec![Op::ControlRemove { path: PathBuf::from("vendor/.gitignore") }],
);
assert_eq!(clones, 1);
assert_eq!(index.controls().expect("observed").refused_len(), 0);
}
#[test]
fn removing_a_subtree_lifts_the_refusals_beneath_it() {
let mut index = Index::new_with_scope("/root", crate::test_support::observing_controls());
let mut line = vec![b'x'; crate::control::DEFAULT_CONTROL_LINE_LIMIT + 1];
line.push(b'\n');
index.apply_ok(&Observation::new(vec![
upsert("vendor", EntryKind::Dir, file_attrs(0, 1)),
upsert("vendor/.gitignore", EntryKind::File, file_attrs(16_386, 1)),
Op::ControlUpsert { path: PathBuf::from("vendor/.gitignore"), source: line },
]));
assert_eq!(index.controls().expect("observed").refused_len(), 1);
let outcome =
index.apply_ok(&Observation::new(vec![Op::Remove { path: PathBuf::from("vendor") }]));
assert_eq!(index.controls().expect("observed").refused_len(), 0);
assert!(outcome.commit.expect("commit").changes.iter().any(|change| matches!(
change,
EffectiveChange::ControlRefusalUpdated { current: None, .. }
)));
}
#[test]
fn one_sweep_reports_one_as_of_time_for_everything_it_verified() {
let mut index = Index::new("/root");
index.set_applying_source(Source::Cached, 1_000);
index.apply_ok(&Observation::new(vec![
Op::Upsert { path: "a".into(), kind: EntryKind::Dir, attrs: file_attrs(0, 1) },
Op::Upsert {
path: "a/kept.txt".into(),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
},
Op::Upsert {
path: "a/changed.txt".into(),
kind: EntryKind::File,
attrs: file_attrs(2, 2),
},
]));
index.set_applying_source(Source::Revalidated, 2_000);
index.begin_reconcile(Path::new("")).expect("begin reconciliation");
index.apply_ok(&Observation::new(vec![
Op::Upsert {
path: "a/kept.txt".into(),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
},
Op::Upsert {
path: "a/changed.txt".into(),
kind: EntryKind::File,
attrs: file_attrs(9, 2),
},
]));
index
.finish_reconcile(
Path::new(""),
0,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish reconciliation");
let kept = index.provenance(Path::new("a/kept.txt")).expect("present");
let changed = index.provenance(Path::new("a/changed.txt")).expect("present");
assert!(kept.is_verified() && changed.is_verified(), "the sweep covered both");
assert_eq!(
kept.observed_at_ns, changed.observed_at_ns,
"one sweep, one as-of time: {kept:?} vs {changed:?}"
);
}
#[test]
fn withdrawn_trust_beats_a_verification_interval() {
let mut index = Index::new("/root");
index.set_applying_source(Source::Cached, 1_000);
index.apply_baseline_ok(&Observation::new(vec![
Op::Upsert { path: PathBuf::from("a"), kind: EntryKind::Dir, attrs: Attrs::default() },
Op::Upsert {
path: PathBuf::from("a/file.txt"),
kind: EntryKind::File,
attrs: Attrs { size: 1, ..Attrs::default() },
},
]));
assert_eq!(
index.provenance(Path::new("a/file.txt")).expect("present").source,
Source::Cached,
"nothing has checked it yet"
);
index
.finish_reconcile(
Path::new(""),
0,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish reconciliation");
let path = Path::new("a/file.txt");
assert_eq!(
index.provenance(path).expect("present").source,
Source::Revalidated,
"a completed sweep covers this path"
);
index.mark_unfresh(Path::new("a"), Freshness::Stale);
let provenance = index.provenance(path).expect("present");
assert!(
!provenance.is_verified(),
"an invalidated path must not read as verified: {provenance:?}"
);
assert_eq!(
provenance.status,
Status::Complete,
"withdrawing trust changes how far to believe the value, not how much of \
the subtree it covers: the cached total still accounts for every entry \
beneath this path, and reporting it as Partial would tell a consumer the \
number is still being built when it is merely unverified"
);
}
#[test]
fn verification_intervals_stay_bounded() {
let mut index = Index::new("/root");
for which in 0..(MAX_VERIFIED_INTERVALS * 2) {
index
.finish_reconcile(
&PathBuf::from(format!("dir-{which}")),
0,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish reconciliation");
}
assert!(
index.verified.len() <= MAX_VERIFIED_INTERVALS,
"interval list grew to {}",
index.verified.len()
);
}
#[test]
fn retained_walk_issues_are_the_same_first_paths_for_every_arrival_order() {
let make = |order: Vec<usize>| {
order
.into_iter()
.map(|number| {
crate::Error::io(
PathBuf::from(format!("/root/file-{number:02}")),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
)
})
.collect::<Vec<_>>()
};
let mut reverse_order: Vec<_> = (0..66).rev().collect();
reverse_order.push(65);
let mut shuffled_order: Vec<_> = (0..66).step_by(2).collect();
shuffled_order.extend((0..66).skip(1).step_by(2));
shuffled_order.push(65);
let mut reverse = Index::new("/root");
let mut reverse_errors = make(reverse_order);
reverse.record_walk_errors(&mut reverse_errors);
let mut shuffled = Index::new("/root");
let mut shuffled_errors = make(shuffled_order);
shuffled.record_walk_errors(&mut shuffled_errors);
let reverse_paths: Vec<_> =
reverse.issues().iter().map(|issue| issue.path.clone().expect("path")).collect();
let shuffled_paths: Vec<_> =
shuffled.issues().iter().map(|issue| issue.path.clone().expect("path")).collect();
assert_eq!(reverse_paths, shuffled_paths);
assert_eq!(reverse_paths.len(), MAX_RETAINED_ISSUES);
assert_eq!(reverse_paths.first().map(PathBuf::as_path), Some(Path::new("file-00")));
assert_eq!(reverse_paths.last().map(PathBuf::as_path), Some(Path::new("file-63")));
assert_eq!(reverse.state().issues.omitted, 2);
assert_eq!(shuffled.state().issues.omitted, 2);
assert_eq!(reverse.issue_epochs.len(), reverse.issues.len());
assert_eq!(shuffled.issue_epochs.len(), shuffled.issues.len());
}
#[test]
fn repeated_partial_root_pass_replaces_bounded_issues_and_omitted_count() {
let root = Path::new("/root");
let mut index = Index::new(root);
let run = |index: &mut Index, range: std::ops::Range<usize>| {
let errors: Vec<_> = range
.clone()
.map(|number| {
crate::Error::io(
root.join(format!("file-{number:02}")),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
)
})
.collect();
let failed: Vec<_> =
range.map(|number| PathBuf::from(format!("file-{number:02}"))).collect();
let (started, _) = index.begin_reconcile(Path::new("")).expect("begin");
index
.finish_reconcile(
Path::new(""),
started,
false,
&[],
&failed,
ReconcileErrors { errors: &errors, terminal: None, disproves_old: true },
)
.expect("finish");
};
run(&mut index, 0..66);
assert_eq!(index.state().issues.omitted, 2);
run(&mut index, 10..76);
assert_eq!(index.state().issues.omitted, 2, "a retry replaces the old omission count");
assert_eq!(index.omitted_issue_epochs.len(), 1, "sequential failures use one bucket");
assert_eq!(index.issues().len(), crate::MAX_RETAINED_ISSUES);
assert_eq!(index.issues()[0].path.as_deref(), Some(Path::new("file-10")));
assert_eq!(index.issues()[63].path.as_deref(), Some(Path::new("file-73")));
}
#[test]
fn partial_pass_preserves_an_issue_published_after_it_began() {
let root = Path::new("/root");
let mut index = Index::new(root);
let (started, _) = index.begin_reconcile(Path::new("")).expect("begin");
index.mark_unfresh(Path::new("concurrent"), Freshness::Stale);
index.retain_issue(Issue::from_error_under(
root,
&crate::Error::io(
root.join("concurrent"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "concurrent"),
),
));
let pass_error = crate::Error::io(
root.join("pass"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "pass"),
);
index
.finish_reconcile(
Path::new(""),
started,
false,
&[],
&[PathBuf::from("pass")],
ReconcileErrors { errors: &[pass_error], terminal: None, disproves_old: true },
)
.expect("finish");
let paths: Vec<_> =
index.issues().iter().filter_map(|issue| issue.path.as_deref()).collect();
assert_eq!(paths, [Path::new("concurrent"), Path::new("pass")]);
}
#[test]
fn older_root_closer_preserves_newer_pass_omissions_and_partial_coverage() {
let root = Path::new("/root");
let mut index = Index::new(root);
for number in 0..66 {
let path = PathBuf::from(format!("a-{number:02}"));
index.retain_issue(Issue::observation_gap(
&path,
crate::InvalidateReason::WatchOverflow,
));
}
index.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
assert_eq!(index.state.issues.omitted, 2);
let (older, _) = index.begin_reconcile(Path::new("")).expect("begin older pass");
let (newer, _) = index.begin_reconcile(Path::new("")).expect("begin newer pass");
let newer_errors = ["z-one", "z-two"].map(|path| {
crate::Error::io(
root.join(path),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
)
});
index
.finish_reconcile(
Path::new(""),
newer,
false,
&[],
&[PathBuf::from("z-one"), PathBuf::from("z-two")],
ReconcileErrors { errors: &newer_errors, terminal: None, disproves_old: true },
)
.expect("finish newer pass");
assert_eq!(index.state.issues.omitted, 2);
index
.finish_reconcile(
Path::new(""),
older,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish older pass");
assert_eq!(index.state.issues.omitted, 2, "the older closer cannot erase newer omissions");
assert_eq!(index.state.coverage, Coverage::Partial(CoverageReason::Inaccessible));
}
#[test]
fn an_unvisited_aborted_scope_does_not_disprove_its_old_issue() {
let root = Path::new("/root");
let mut index = Index::new(root);
index.retain_issue(Issue::from_error_under(
root,
&crate::Error::io(
root.join("later/blocked"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "old failure"),
),
));
let (started, _) = index.begin_reconcile(Path::new("later")).expect("begin later scope");
index
.finish_reconcile(
Path::new("later"),
started,
false,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: false },
)
.expect("close skipped scope");
assert_eq!(index.issues().len(), 1);
assert_eq!(index.issues()[0].path.as_deref(), Some(Path::new("later/blocked")));
}
#[test]
fn older_pass_cannot_publish_errors_after_newer_clean_verification() {
let root = Path::new("/root");
let mut index = Index::new(root);
let (older, _) = index.begin_reconcile(Path::new("")).expect("begin older pass");
let (newer, _) = index.begin_reconcile(Path::new("")).expect("begin newer pass");
index
.finish_reconcile(
Path::new(""),
newer,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish newer pass");
let stale_error = crate::Error::io(
root.join("stale"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "older failure"),
);
index
.finish_reconcile(
Path::new(""),
older,
false,
&[],
&[PathBuf::from("stale")],
ReconcileErrors { errors: &[stale_error], terminal: None, disproves_old: true },
)
.expect("finish superseded older pass");
assert!(index.issues().is_empty());
assert_eq!(index.state.coverage, Coverage::Complete);
assert_eq!(index.state.freshness, Freshness::Fresh);
}
#[test]
fn newer_unchanged_verification_refuses_an_older_conditional_fact() {
let mut index = Index::new("/root");
index
.apply(&Observation::new(vec![Op::Upsert {
path: PathBuf::from("same"),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
}]))
.expect("fixture");
let handle = IndexHandle::new(index);
let baseline = handle.expectation(Path::new("same")).expect("baseline");
let (older, _) = handle.begin_reconcile(Path::new("")).expect("begin older pass");
let (newer, _) = handle.begin_reconcile(Path::new("")).expect("begin newer pass");
handle
.finish_reconcile(
Path::new(""),
newer,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish unchanged newer pass");
let stale = handle
.apply_reconcile(
older,
&Observation::from_ops(vec![ObservationOp::if_state(
Op::Remove { path: PathBuf::from("same") },
baseline,
)]),
)
.expect("arbitrate older fact");
assert_eq!(stale.stats.stale, 1);
assert!(stale.commit.is_none());
assert!(handle.attrs(Path::new("same")).expect("attrs").is_some());
}
#[test]
fn newer_disjoint_verification_does_not_refuse_an_older_fact() {
let mut index = Index::new("/root");
index
.apply(&Observation::new(vec![
Op::Upsert {
path: PathBuf::from("a"),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
},
Op::Upsert {
path: PathBuf::from("b"),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
},
]))
.expect("fixture");
let handle = IndexHandle::new(index);
let baseline = handle.expectation(Path::new("a")).expect("baseline");
let (older, _) = handle.begin_reconcile(Path::new("a")).expect("begin a");
let (newer, _) = handle.begin_reconcile(Path::new("b")).expect("begin b");
handle
.finish_reconcile(
Path::new("b"),
newer,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("finish b");
let applied = handle
.apply_reconcile(
older,
&Observation::from_ops(vec![ObservationOp::if_state(
Op::Remove { path: PathBuf::from("a") },
baseline,
)]),
)
.expect("apply disjoint a fact");
assert_eq!(applied.stats.removed, 1);
assert_eq!(applied.stats.stale, 0);
assert!(handle.attrs(Path::new("a")).expect("attrs").is_none());
}
#[test]
fn newer_child_verification_preserves_older_sibling_failure() {
let root = Path::new("/root");
let mut index = Index::new(root);
index.apply_ok(&Observation::new(
["a", "a/old", "b", "b/blocked", "healthy"]
.map(|path| Op::Upsert {
path: PathBuf::from(path),
kind: EntryKind::Dir,
attrs: Attrs::default(),
})
.to_vec(),
));
index.set_initial_scan_freshness(&[]);
let (older, _) = index.begin_reconcile(Path::new("")).expect("older root");
let (newer, _) = index.begin_reconcile(Path::new("a")).expect("newer child");
index
.finish_reconcile(
Path::new("a"),
newer,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("verify a");
let errors = ["a/old", "b/blocked"].map(|path| {
crate::Error::io(
root.join(path),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "failed read"),
)
});
index
.finish_reconcile(
Path::new(""),
older,
false,
&[],
&[PathBuf::from("a/old"), PathBuf::from("b/blocked")],
ReconcileErrors { errors: &errors, terminal: None, disproves_old: true },
)
.expect("finish older root");
for path in ["", "a", "a/old", "b", "healthy"] {
assert_eq!(index.directory_complete(Path::new(path)), Some(true), "{path}");
}
assert_eq!(index.directory_complete(Path::new("b/blocked")), Some(false));
assert_eq!(index.issues().len(), 1);
assert_eq!(index.issues()[0].path.as_deref(), Some(Path::new("b/blocked")));
assert_eq!(index.state.coverage, Coverage::Partial(CoverageReason::Inaccessible));
assert_eq!(index.freshness_at(Path::new("a")), Freshness::Fresh);
assert_eq!(index.freshness_at(Path::new("b/blocked")), Freshness::Partial);
}
#[test]
fn failure_published_after_a_newer_pass_began_keeps_its_issue_with_its_mark() {
let root = Path::new("/root");
let mut index = Index::new(root);
index.apply_ok(&Observation::new(
["x", "x/deep", "healthy"]
.map(|path| Op::Upsert {
path: PathBuf::from(path),
kind: EntryKind::Dir,
attrs: Attrs::default(),
})
.to_vec(),
));
index.set_initial_scan_freshness(&[]);
let (older_root, _) = index.begin_reconcile(Path::new("")).expect("older root");
let (newer_child, _) = index.begin_reconcile(Path::new("x")).expect("newer child");
let error = crate::Error::io(
root.join("x"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "failed read"),
);
index
.finish_reconcile(
Path::new(""),
older_root,
false,
&[],
&[PathBuf::from("x")],
ReconcileErrors { errors: &[error], terminal: None, disproves_old: true },
)
.expect("older root fails on x");
assert_eq!(index.freshness_at(Path::new("x")), Freshness::Partial);
assert_eq!(index.issues().len(), 1);
index
.finish_reconcile(
Path::new("x"),
newer_child,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("newer child verifies clean");
let explained = !index.issues().is_empty();
let partial = index.freshness_at(Path::new("x")) == Freshness::Partial;
assert_eq!(
partial,
explained,
"a surviving partial mark and its issue must be kept or dropped together: \
partial={partial}, issues={:?}",
index.issues()
);
assert!(partial, "the mark minted after the child pass began is the newer claim");
assert_eq!(index.issues()[0].path.as_deref(), Some(Path::new("x")));
assert_eq!(index.state.coverage, Coverage::Partial(CoverageReason::Inaccessible));
assert_eq!(index.state.freshness, Freshness::Partial);
let request = crate::query::Request::new(
crate::query::Basis::held_by(&index),
crate::query::Query::default(),
std::time::UNIX_EPOCH,
);
let status = crate::query::TreeStatus::of(&index, &request);
assert!(!status.complete);
assert_eq!(status.coverage, Coverage::Partial(CoverageReason::Inaccessible));
assert_eq!(status.errors.len(), 1, "an incomplete status names its cause");
}
#[test]
fn cold_scan_failure_does_not_verify_unknown_descendants_or_unscoped_work() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(
["blocked", "blocked/nested", "healthy"]
.map(|path| Op::Upsert {
path: PathBuf::from(path),
kind: EntryKind::Dir,
attrs: Attrs::default(),
})
.to_vec(),
));
let error = crate::Error::io(
PathBuf::from("/root/blocked"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "failed listing"),
);
index.set_initial_scan_freshness(&[error]);
assert_eq!(index.directory_complete(Path::new("healthy")), Some(true));
assert_eq!(index.directory_complete(Path::new("")), Some(true));
assert_eq!(index.freshness_at(Path::new("")), Freshness::Partial);
for path in ["blocked", "blocked/nested"] {
assert_eq!(index.directory_complete(Path::new(path)), Some(false), "{path}");
assert_eq!(index.freshness_at(Path::new(path)), Freshness::Partial, "{path}");
}
index.set_initial_scan_freshness(&[crate::Error::Snapshot("unscoped failure".into())]);
assert_eq!(index.directory_complete(Path::new("healthy")), Some(false));
assert_eq!(index.freshness_at(Path::new("healthy")), Freshness::Partial);
}
#[test]
fn unscoped_failure_publishes_listing_withdrawal_when_root_state_is_unchanged() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![Op::Upsert {
path: PathBuf::from("healthy"),
kind: EntryKind::Dir,
attrs: Attrs::default(),
}]));
index.set_initial_scan_freshness(&[]);
index.mark_unfresh(Path::new("elsewhere"), Freshness::Partial);
index.state.freshness = Freshness::Partial;
index.state.coverage = Coverage::Partial(CoverageReason::Inaccessible);
let error = crate::Error::Snapshot("unscoped failure".into());
index.retain_issue(Issue::from_error_under(&index.root_path, &error));
let progress = index.state.progress;
let (epoch, _) = index.begin_reconcile(Path::new("")).expect("begin partial root");
let before = index.state;
let clock = index.clock;
let finished = index
.finish_reconcile(
Path::new(""),
epoch,
false,
&[],
&[],
ReconcileErrors { errors: &[error], terminal: None, disproves_old: true },
)
.expect("finish failure");
assert_eq!(index.state, before, "aggregate root state remains identical");
assert_eq!(index.state.progress, progress, "discovery progress is cumulative");
assert_eq!(index.directory_complete(Path::new("healthy")), Some(false));
let commit = finished.commit.expect("withdrawal must publish even without another effect");
assert!(index.clock > clock);
assert_eq!(commit.clock, index.clock);
assert!(
commit
.state
.iter()
.all(|effect| matches!(effect, StateTransition::DirectoryIncomplete { .. }))
);
assert!(
commit
.state
.contains(&StateTransition::DirectoryIncomplete { path: PathBuf::from("healthy") })
);
assert!(commit.impact.dirty_paths.contains(&PathBuf::from("healthy")));
}
#[test]
fn omitted_failed_entry_withdraws_parent_listing_without_tainting_siblings() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![Op::Upsert {
path: PathBuf::from("healthy"),
kind: EntryKind::Dir,
attrs: Attrs::default(),
}]));
let error = crate::Error::io(
PathBuf::from("/root/missing"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "metadata failed"),
);
index.set_initial_scan_freshness(&[error]);
assert_eq!(index.directory_complete(Path::new("")), Some(false));
assert_eq!(index.directory_complete(Path::new("healthy")), Some(true));
index.set_initial_scan_freshness(&[]);
let (epoch, _) = index.begin_reconcile(Path::new("")).expect("begin root");
let error = crate::Error::io(
PathBuf::from("/root/missing"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "metadata failed"),
);
index
.finish_reconcile(
Path::new(""),
epoch,
false,
&[],
&[PathBuf::from("missing")],
ReconcileErrors { errors: &[error], terminal: None, disproves_old: true },
)
.expect("partial root");
assert_eq!(index.directory_complete(Path::new("")), Some(false));
assert_eq!(index.directory_complete(Path::new("healthy")), Some(true));
}
#[test]
fn complete_older_root_does_not_verify_a_newer_failed_child() {
let root = Path::new("/root");
let mut index = Index::new(root);
let (older, _) = index.begin_reconcile(Path::new("")).expect("older root");
let (newer, _) = index.begin_reconcile(Path::new("child")).expect("newer child");
let error = crate::Error::io(
root.join("child"),
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "new failure"),
);
index
.finish_reconcile(
Path::new("child"),
newer,
false,
&[],
&[PathBuf::from("child")],
ReconcileErrors { errors: &[error], terminal: None, disproves_old: true },
)
.expect("failed child");
let finish = index
.finish_reconcile(
Path::new(""),
older,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("complete older walk");
assert_eq!(index.issues().len(), 1);
assert_eq!(index.freshness_at(Path::new("child")), Freshness::Partial);
assert_eq!(index.state.coverage, Coverage::Partial(CoverageReason::Inaccessible));
assert!(!finish.commit.iter().flat_map(|commit| commit.state.iter()).any(|state| {
matches!(state, StateTransition::Verified { path } if path.as_os_str().is_empty())
}));
}
#[test]
fn reconciliation_scope_budget_preserves_issues_and_newer_facts() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![Op::Upsert {
path: PathBuf::from("kept"),
kind: EntryKind::File,
attrs: file_attrs(1, 1),
}]));
index.retain_issue(Issue::provider_failure(
Some(Path::new("unvisited")),
"earlier failure".into(),
));
let (older, _) = index.begin_reconcile(Path::new("")).expect("older pass");
let budget = index.active_reconciles[&older].scope_budget;
assert_eq!(budget, 2, "root and kept file define the evidence budget");
for number in 0..100 {
let path = PathBuf::from(format!("missing-{number}"));
let (newer, _) = index.begin_reconcile(&path).expect("newer pass");
index
.finish_reconcile(
&path,
newer,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("newer verification");
if let ReconcileEvidence::Scopes(scopes) = &index.active_reconciles[&older].evidence {
assert!(scopes.len() <= budget);
}
}
assert!(matches!(index.active_reconciles[&older].evidence, ReconcileEvidence::Retry));
index.apply_ok(&Observation::new(vec![Op::Upsert {
path: PathBuf::from("kept"),
kind: EntryKind::File,
attrs: file_attrs(9, 2),
}]));
let finished = index
.finish_reconcile(
Path::new(""),
older,
true,
&[],
&[],
ReconcileErrors { errors: &[], terminal: None, disproves_old: true },
)
.expect("close interrupted pass");
assert!(finished.retry);
assert!(finished.commit.is_some());
assert_eq!(index.total_scalars().bytes, 9);
assert!(
index
.issues()
.iter()
.any(|issue| issue.path.as_deref() == Some(Path::new("unvisited")))
);
assert_eq!(index.state.coverage, Coverage::Partial(CoverageReason::Inaccessible));
assert!(index.active_reconciles.is_empty(), "closed passes retain no shadow history");
}
fn sized(size: u64) -> Attrs {
Attrs { size, allocated: size, mtime_ns: 1, ctime_ns: 1, inode: size ^ 7, dev: 1 }
}
fn assert_unrepresentable(error: &crate::Error, path: &str, counter: &str) {
match error {
crate::Error::UnrepresentableTotal { path: at, counter: which } => {
assert_eq!(at, Path::new(path));
assert_eq!(*which, counter);
}
other => panic!("expected an unrepresentable-total refusal, got {other:?}"),
}
}
fn observable_state(index: &Index) -> (u64, Clock, RollUp, Vec<PathBuf>, usize) {
let mut paths: Vec<PathBuf> = Vec::new();
let mut pending = vec![EntryId::ROOT];
while let Some(id) = pending.pop() {
paths.push(index.path_of(id).expect("live entry"));
if index.entry(id).kind.is_dir() {
pending.extend(index.child_ids(id));
}
}
paths.sort();
(index.len(), index.clock(), index.total(), paths, index.journal.len())
}
#[test]
fn a_batch_whose_byte_total_would_exceed_u64_is_refused_before_any_mutation() {
let mut index = Index::new("/root");
let before = observable_state(&index);
let error = index
.apply(&Observation::new(vec![
upsert("a", EntryKind::File, sized(u64::MAX)),
upsert("b", EntryKind::File, sized(1)),
]))
.expect_err("two files summing past u64::MAX cannot be represented");
assert_unrepresentable(&error, "b", "bytes");
assert_eq!(observable_state(&index), before, "a refused batch changes nothing");
index.apply_ok(&Observation::new(vec![upsert("a", EntryKind::File, sized(u64::MAX))]));
let before = observable_state(&index);
assert_eq!(before.2.bytes, u64::MAX);
let error = index
.apply(&Observation::new(vec![
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/b", EntryKind::File, sized(1)),
]))
.expect_err("one more byte is unrepresentable");
assert_unrepresentable(&error, "dir/b", "bytes");
assert_eq!(observable_state(&index), before);
assert!(index.lookup(Path::new("dir")).is_none(), "the batch is fault-atomic");
let error = index
.apply(&Observation::new(vec![upsert(
"c",
EntryKind::File,
Attrs { size: 0, allocated: 1, ..sized(0) },
)]))
.expect_err("allocated bytes overflow on their own");
assert_unrepresentable(&error, "c", "allocated bytes");
assert_eq!(observable_state(&index), before);
}
#[test]
fn exact_fit_replacement_removal_and_kind_change_batches_are_accepted() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("a", EntryKind::File, sized(u64::MAX))]));
index.apply_ok(&Observation::new(vec![
upsert("a", EntryKind::File, sized(1)),
upsert("b", EntryKind::File, sized(u64::MAX - 1)),
]));
assert_eq!(index.total().bytes, u64::MAX);
assert_eq!(index.total().files, 2);
let before = observable_state(&index);
let error = index
.apply(&Observation::new(vec![
upsert("c", EntryKind::File, sized(5)),
Op::Remove { path: PathBuf::from("b") },
]))
.expect_err("an insertion before the removal that makes room for it");
assert_unrepresentable(&error, "c", "bytes");
assert_eq!(observable_state(&index), before);
index.apply_ok(&Observation::new(vec![
Op::Remove { path: PathBuf::from("b") },
upsert("c", EntryKind::File, sized(u64::MAX - 1)),
]));
assert_eq!(index.total().bytes, u64::MAX);
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("d", EntryKind::Dir, Attrs::default()),
upsert("d/f", EntryKind::File, sized(u64::MAX)),
]));
index.apply_ok(&Observation::new(vec![
upsert("d", EntryKind::File, sized(5)),
upsert("e", EntryKind::File, sized(u64::MAX - 5)),
]));
assert_eq!(index.total().bytes, u64::MAX);
assert_eq!((index.total().files, index.total().dirs), (2, 0));
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("x", EntryKind::File, sized(u64::MAX)),
upsert("x", EntryKind::File, sized(1)),
upsert("y", EntryKind::File, sized(u64::MAX - 1)),
]));
assert_eq!(index.total().bytes, u64::MAX);
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![
upsert("d", EntryKind::Dir, Attrs::default()),
upsert("d/f", EntryKind::File, sized(u64::MAX - 10)),
]));
index.apply_ok(&Observation::new(vec![
upsert("d/g", EntryKind::File, sized(10)),
Op::Remove { path: PathBuf::from("d") },
upsert("z", EntryKind::File, sized(u64::MAX)),
]));
assert_eq!(index.total().bytes, u64::MAX);
assert_eq!((index.total().files, index.total().dirs), (1, 0));
}
#[test]
fn a_stale_conditional_upsert_does_not_count_toward_the_projected_total() {
let mut index = Index::new("/root");
index.apply_ok(&Observation::new(vec![upsert("a", EntryKind::File, sized(5))]));
let stale = index.expectation(Path::new("b"));
index.apply_ok(&Observation::new(vec![upsert("b", EntryKind::File, sized(1))]));
let outcome = index.apply_ok(&Observation::from_ops(vec![
ObservationOp::if_state(upsert("b", EntryKind::File, sized(u64::MAX)), stale),
ObservationOp::unconditional(upsert("c", EntryKind::File, sized(u64::MAX - 6))),
]));
assert_eq!(outcome.stats.stale, 1);
assert_eq!(index.total().bytes, u64::MAX);
}
#[test]
fn the_baseline_and_scanner_lanes_refuse_unrepresentable_totals() {
let mut index = Index::new("/root");
let before = observable_state(&index);
let error = index
.apply_baseline(&Observation::new(vec![
upsert("a", EntryKind::File, sized(u64::MAX)),
upsert("b", EntryKind::File, sized(1)),
]))
.expect_err("the baseline lane refuses too");
assert_unrepresentable(&error, "b", "bytes");
assert_eq!(observable_state(&index), before);
let error = index
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/a", EntryKind::File, sized(u64::MAX)),
upsert("dir/b", EntryKind::File, sized(1)),
]))
.expect_err("the scanner lane refuses too");
assert_unrepresentable(&error, "dir/b", "bytes");
assert_eq!(observable_state(&index), before);
index
.apply_scanner_baseline(crate::scan::ScannerBatch::from_ops(vec![
upsert("dir", EntryKind::Dir, Attrs::default()),
upsert("dir/a", EntryKind::File, sized(u64::MAX - 1)),
upsert("dir/b", EntryKind::File, sized(1)),
]))
.expect("an exact fit");
assert_eq!(index.total().bytes, u64::MAX);
}
fn tree_for_analysis() -> Index {
let mut index = Index::new("/root");
let mut ops = vec![upsert("z.txt", EntryKind::File, file_attrs(3, 1))];
for directory in ["a", "b", "c"] {
ops.push(upsert(directory, EntryKind::Dir, Attrs::default()));
ops.push(upsert(&format!("{directory}/deep"), EntryKind::Dir, Attrs::default()));
for file in 0..4 {
ops.push(upsert(
&format!("{directory}/f{file}.rs"),
EntryKind::File,
file_attrs(10 + file, 1),
));
}
ops.push(upsert(&format!("{directory}/deep/d.md"), EntryKind::File, file_attrs(7, 2)));
ops.push(upsert(&format!("{directory}/link"), EntryKind::Symlink, Attrs::default()));
}
index.apply_ok(&Observation::new(ops));
index
}
#[test]
fn analysis_candidates_come_in_bounded_batches_in_walk_order() {
let index = tree_for_analysis();
let request = crate::content::AnalysisRequest {
profile: AnalysisSet::NONE.with_lines(),
..crate::content::AnalysisRequest::default()
};
let whole: Vec<PathBuf> = index
.pending_analysis_candidates(request)
.into_iter()
.map(|candidate| candidate.relative_path)
.collect();
assert_eq!(whole.len(), 16, "one root file and five files under each directory");
assert_eq!(index.count_pending_analysis_candidates(request), 16);
for limit in [1, 3, 5, 16, 17, usize::MAX] {
let mut walk = AnalysisWalk::start();
let mut collected = Vec::new();
let mut batch_count = 0;
loop {
let batch = index.next_analysis_candidates(request, &mut walk, limit);
if batch.is_empty() {
break;
}
assert!(batch.len() <= limit, "a batch is bounded by its limit");
batch_count += 1;
collected.extend(batch.into_iter().map(|candidate| candidate.relative_path));
}
assert_eq!(
collected, whole,
"batches of {limit} walk the same files in the same order"
);
assert_eq!(batch_count, whole.len().div_ceil(limit.min(whole.len())));
assert!(
index.next_analysis_candidates(request, &mut walk, limit).is_empty(),
"a finished walk stays finished"
);
}
}
#[test]
fn a_long_listing_is_resumed_where_the_last_batch_stopped() {
let names: Vec<String> = (0..11).map(|file| format!("f{file:02}.rs")).collect();
let request = crate::content::AnalysisRequest {
profile: AnalysisSet::NONE.with_lines(),
..crate::content::AnalysisRequest::default()
};
let sorted = {
let mut builder = DetachedIndexBuilder::new(
"/root",
ScanScope::default(),
crate::classify::TypeRegistry::compiled_shared(),
);
let mut listing = crate::scan::DetachedDirectory {
path: PathBuf::new(),
children: (0_u32..)
.zip(&names)
.map(|(position, name)| crate::scan::DetachedChild {
name: OsString::from(name),
kind: EntryKind::File,
attrs: file_attrs(10, 1),
position,
})
.collect(),
control: None,
};
builder.push_directory(&mut listing).expect("listing");
builder.finish()
};
let mut mapped = Index::new("/root");
mapped.apply_ok(&Observation::new(
names.iter().map(|name| upsert(name, EntryKind::File, file_attrs(10, 1))).collect(),
));
for (storage, index) in [("sorted", &sorted), ("mapped", &mapped)] {
ANALYSIS_CHILD_STEPS.with(|steps| steps.set(0));
let mut walk = AnalysisWalk::start();
let mut walked = Vec::new();
let mut batches = 0;
loop {
let batch = index.next_analysis_candidates(request, &mut walk, 3);
if batch.is_empty() {
break;
}
batches += 1;
walked.extend(batch.into_iter().map(|candidate| candidate.relative_path));
}
assert_eq!(batches, 4, "{storage}: eleven files in batches of three");
assert_eq!(
walked,
names.iter().map(PathBuf::from).collect::<Vec<_>>(),
"{storage}: every file once, in listing order"
);
assert_eq!(
ANALYSIS_CHILD_STEPS.with(std::cell::Cell::get),
11,
"{storage}: each child is stepped over once across all four batches"
);
}
}
#[test]
fn batches_skip_what_the_content_tier_already_holds() {
let root = tempfile::tempdir().expect("root");
for name in ["one.rs", "two.rs", "three.rs"] {
std::fs::write(root.path().join(name), b"fn main() {}\n").expect("file");
}
let (mut index, _) =
crate::scan::scan_into_index(root.path(), &crate::ScanConfig::default()).expect("scan");
let request =
crate::content::AnalysisRequest { profile: AnalysisSet::NONE.with_lines(), workers: 1 };
assert_eq!(index.count_pending_analysis_candidates(request), 3);
crate::content::analyze_index(&mut index, request);
assert_eq!(index.count_pending_analysis_candidates(request), 0);
assert!(index.next_analysis_candidates(request, &mut AnalysisWalk::start(), 2).is_empty());
std::fs::write(root.path().join("two.rs"), b"fn main() { changed(); }\n").expect("grow");
let (rescanned, _) =
crate::scan::scan_into_index(root.path(), &crate::ScanConfig::default())
.expect("rescan");
index
.apply(&Observation::new(vec![upsert(
"two.rs",
EntryKind::File,
*rescanned.attrs(Path::new("two.rs")).expect("attrs"),
)]))
.expect("apply");
assert_eq!(index.count_pending_analysis_candidates(request), 1);
let batch = index.next_analysis_candidates(request, &mut AnalysisWalk::start(), 2);
assert_eq!(
batch.iter().map(|candidate| candidate.relative_path.clone()).collect::<Vec<_>>(),
[PathBuf::from("two.rs")]
);
}
}