use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalOutput {
pub path: PathBuf,
pub pre_existing: bool,
pub replaced: bool,
}
impl LocalOutput {
pub fn created(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
pre_existing: false,
replaced: false,
}
}
pub fn pre_existing(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
pre_existing: true,
replaced: false,
}
}
pub fn replaced(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
pre_existing: true,
replaced: true,
}
}
}
pub fn probe_pre_existing(path: &Path) -> bool {
match std::fs::symlink_metadata(path) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(_) => true,
}
}
#[derive(Debug, Default)]
pub struct LocalOutputLog {
seen: Mutex<BTreeMap<PathBuf, (bool, bool, usize)>>,
}
impl LocalOutputLog {
pub fn new() -> Self {
Self::default()
}
pub fn record_open(&self, path: impl Into<PathBuf>, pre_existing: bool) {
self.record_open_with(path, pre_existing, false);
}
pub fn record_open_with(&self, path: impl Into<PathBuf>, pre_existing: bool, truncates: bool) {
let path = path.into();
if let Ok(mut seen) = self.seen.lock() {
let next = seen.len();
seen.entry(path).or_insert((pre_existing, truncates, next));
}
}
pub fn record_open_probing(&self, path: impl Into<PathBuf>) {
self.record_open_probing_with(path, false);
}
pub fn record_open_probing_with(&self, path: impl Into<PathBuf>, truncates: bool) {
let path = path.into();
let known = self
.seen
.lock()
.map(|seen| seen.contains_key(&path))
.unwrap_or(true);
if !known {
let pre_existing = probe_pre_existing(&path);
self.record_open_with(path, pre_existing, truncates);
}
}
pub fn snapshot(&self) -> Vec<LocalOutput> {
let Ok(seen) = self.seen.lock() else {
return Vec::new();
};
let mut rows: Vec<(usize, LocalOutput)> = seen
.iter()
.map(|(path, (pre_existing, replaced, idx))| {
(
*idx,
LocalOutput {
path: path.clone(),
pre_existing: *pre_existing,
replaced: *pre_existing && *replaced,
},
)
})
.collect();
rows.sort_by_key(|(idx, _)| *idx);
rows.into_iter().map(|(_, out)| out).collect()
}
pub fn is_empty(&self) -> bool {
self.seen.lock().map(|s| s.is_empty()).unwrap_or(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn records_in_first_seen_order() {
let log = LocalOutputLog::new();
log.record_open("/tmp/b.jsonl", false);
log.record_open("/tmp/a.jsonl", false);
let snap = log.snapshot();
assert_eq!(
snap.iter().map(|o| o.path.clone()).collect::<Vec<_>>(),
vec![PathBuf::from("/tmp/b.jsonl"), PathBuf::from("/tmp/a.jsonl")],
"insertion order, not the BTreeMap's sort order"
);
}
#[test]
fn dedupes_by_path() {
let log = LocalOutputLog::new();
log.record_open("/tmp/a.jsonl", false);
log.record_open("/tmp/a.jsonl", false);
assert_eq!(log.snapshot().len(), 1);
}
#[test]
fn first_open_classification_is_sticky() {
let log = LocalOutputLog::new();
log.record_open("/tmp/a.jsonl", false);
log.record_open("/tmp/a.jsonl", true);
assert!(!log.snapshot()[0].pre_existing);
let log = LocalOutputLog::new();
log.record_open("/tmp/theirs.csv", true);
log.record_open("/tmp/theirs.csv", false);
assert!(log.snapshot()[0].pre_existing);
}
#[test]
fn replaced_is_first_open_wins_and_only_meaningful_for_pre_existing() {
let log = LocalOutputLog::new();
log.record_open_with("/tmp/theirs.jsonl", true, true);
log.record_open_with("/tmp/theirs.jsonl", true, false);
log.record_open_with("/tmp/ours.jsonl", false, true);
log.record_open("/tmp/appended.jsonl", true);
let snap = log.snapshot();
assert_eq!(snap[0], LocalOutput::replaced("/tmp/theirs.jsonl"));
assert_eq!(snap[1], LocalOutput::created("/tmp/ours.jsonl"));
assert_eq!(snap[2], LocalOutput::pre_existing("/tmp/appended.jsonl"));
}
#[test]
fn probing_with_truncation_flags_an_existing_file_as_replaced() {
let dir = std::env::temp_dir().join(format!("faucet-lo-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let existing = dir.join("existing.jsonl");
std::fs::write(&existing, b"x").unwrap();
let log = LocalOutputLog::new();
log.record_open_probing_with(&existing, true);
log.record_open_probing_with(dir.join("new.jsonl"), true);
let snap = log.snapshot();
assert!(snap[0].pre_existing && snap[0].replaced);
assert!(!snap[1].pre_existing && !snap[1].replaced);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn empty_log_reports_empty() {
let log = LocalOutputLog::new();
assert!(log.is_empty());
assert!(log.snapshot().is_empty());
log.record_open("/tmp/a.jsonl", false);
assert!(!log.is_empty());
}
#[test]
fn probe_reports_absent_and_present_paths() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope.jsonl");
assert!(!probe_pre_existing(&missing));
std::fs::write(&missing, b"").unwrap();
assert!(probe_pre_existing(&missing));
}
#[test]
fn probe_reports_a_dangling_symlink_as_pre_existing() {
let dir = tempfile::tempdir().unwrap();
let link = dir.path().join("link.jsonl");
#[cfg(unix)]
std::os::unix::fs::symlink(dir.path().join("absent-target"), &link).unwrap();
#[cfg(not(unix))]
std::fs::write(&link, b"").unwrap();
assert!(probe_pre_existing(&link));
}
#[test]
fn record_open_probing_probes_once_then_reuses() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.jsonl");
let log = LocalOutputLog::new();
log.record_open_probing(&path);
assert!(!log.snapshot()[0].pre_existing);
std::fs::write(&path, b"{}\n").unwrap();
log.record_open_probing(&path);
assert_eq!(log.snapshot().len(), 1);
assert!(!log.snapshot()[0].pre_existing);
}
#[test]
fn record_open_probing_marks_a_file_faucet_did_not_create() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("theirs.jsonl");
std::fs::write(&path, b"existing\n").unwrap();
let log = LocalOutputLog::new();
log.record_open_probing(&path);
assert!(log.snapshot()[0].pre_existing);
}
#[test]
fn constructors_set_the_flag() {
assert!(!LocalOutput::created("/tmp/a").pre_existing);
assert!(LocalOutput::pre_existing("/tmp/a").pre_existing);
}
}