weavatrix-scan 0.2.0

Deterministic, safe repository scanner for code intelligence
Documentation
use super::*;
use crate::{CancellationToken, ScanTermination};
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(),
    )
    .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(),
    )
    .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(),
    )
    .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(),
    )
    .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()).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(),
    );
    assert!(aborted.is_err());
    let _ = fs::remove_dir_all(root);
}

fn scanned(absolute: std::path::PathBuf, relative: &str) -> ScannedFile {
    ScannedFile {
        absolute,
        relative: relative.to_owned(),
        bytes: 16,
        content_hash: None,
    }
}

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
}