weavatrix-scan 0.4.3

Deterministic, safe repository scanner for code intelligence
Documentation
use super::*;
use crate::{CancellationToken, ScanTermination};
use std::fs;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

#[test]
fn content_inspection_honors_cancel_timeout_and_evidence_modes() {
    let root = fixture("content-stop");
    let first = root.join("first.rs");
    let second = root.join("second.rs");
    fs::write(&first, "fn first() {}\n").unwrap();
    fs::write(&second, "fn second() {}\n").unwrap();
    let files = vec![scanned(first, "first.rs"), scanned(second, "second.rs")];

    let token = CancellationToken::new();
    token.cancel();
    let cancelled = inspect_files(
        files.clone(),
        &ScanOptions::default().with_cancellation(token),
        Instant::now(),
        None,
    )
    .unwrap();
    assert_eq!(cancelled.termination, Some(ScanTermination::Cancelled));
    assert!(cancelled.files.is_empty());
    assert_eq!(cancelled.skipped.len(), 2);

    let timeout = inspect_files(
        files.clone(),
        &ScanOptions::default().with_timeout(Some(Duration::ZERO)),
        Instant::now(),
        None,
    )
    .unwrap();
    assert_eq!(timeout.termination, Some(ScanTermination::Timeout));
    assert_eq!(timeout.skipped.len(), 2);

    let selected = inspect_files(
        files,
        &ScanOptions::default()
            .with_timeout(Some(Duration::ZERO))
            .selected_files_only(),
        Instant::now(),
        None,
    )
    .unwrap();
    assert!(selected.skipped.is_empty());

    assert_eq!(termination_code(ScanTermination::MaxEntries), 0);
    assert_eq!(termination_code(ScanTermination::MaxTotalBytes), 0);
    let _ = fs::remove_dir_all(root);
}

#[test]
fn content_read_errors_continue_or_abort_in_both_read_modes() {
    let root = fixture("content-errors");
    let missing = scanned(root.join("missing.rs"), "missing.rs");

    let continued = inspect_files(
        vec![missing.clone()],
        &ScanOptions::default(),
        Instant::now(),
        None,
    )
    .unwrap();
    assert!(continued.files.is_empty());
    assert_eq!(continued.warnings.len(), 1);
    assert_eq!(continued.skipped[0].kind, SkipKind::IoError);

    let detect_only = ScanOptions {
        hash_file_contents: false,
        ..ScanOptions::default()
    };
    let continued =
        inspect_files(vec![missing.clone()], &detect_only, Instant::now(), None).unwrap();
    assert!(continued.files.is_empty());
    assert_eq!(continued.warnings.len(), 1);

    let aborted = inspect_files(
        vec![missing],
        &ScanOptions::default().with_error_policy(ErrorPolicy::Abort),
        Instant::now(),
        None,
    );
    assert!(aborted.is_err());
    let _ = fs::remove_dir_all(root);
}

#[test]
fn content_changes_between_discovery_and_read_are_typed_or_abort() {
    let root = fixture("content-concurrent");
    let path = root.join("source.rs");
    fs::write(&path, "before\n").unwrap();
    let discovered = scanned(path.clone(), "source.rs");
    fs::write(&path, "changed after discovery\n").unwrap();

    let continued = inspect_files(
        vec![discovered.clone()],
        &ScanOptions::default(),
        Instant::now(),
        None,
    )
    .unwrap();
    assert!(continued.files.is_empty());
    assert_eq!(continued.skipped[0].kind, SkipKind::ConcurrentModification);
    assert_eq!(continued.warnings.len(), 1);
    assert_eq!(continued.cache.content_reads, 1);

    let aborted = inspect_files(
        vec![discovered],
        &ScanOptions::default().with_error_policy(ErrorPolicy::Abort),
        Instant::now(),
        None,
    );
    assert!(matches!(aborted, Err(Error::ConcurrentModification(_))));
    let _ = fs::remove_dir_all(root);
}

fn scanned(absolute: std::path::PathBuf, relative: &str) -> ScannedFile {
    let metadata = fs::metadata(&absolute).ok();
    ScannedFile {
        absolute,
        relative: relative.to_owned(),
        bytes: metadata.as_ref().map_or(0, std::fs::Metadata::len),
        content_hash: None,
        content_fingerprint: None,
        version: metadata.as_ref().map_or_else(
            crate::FileVersion::default,
            crate::file_version::from_metadata,
        ),
        binary_checked: false,
    }
}

fn fixture(name: &str) -> std::path::PathBuf {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let root = std::env::temp_dir().join(format!("{name}-{}-{nonce}", std::process::id()));
    fs::create_dir_all(&root).unwrap();
    root
}