keyhog-sources 0.5.42

keyhog-sources: pluggable input backends for KeyHog (git, S3, GCS, Azure Blob, Docker, Web)
Documentation
use keyhog_core::{SourceCoverageGapKind, SourceError};

use super::{
    clone_materialization_cap, clone_materialization_truncated, CloneMaterializationCap,
    CloneMaterializationGuard,
};

#[test]
fn guard_enforces_byte_and_entry_caps() {
    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("clone");
    std::fs::create_dir(&root).expect("clone root");
    std::fs::write(root.join("payload"), b"0123456789").expect("payload");

    let bytes = clone_materialization_cap(CloneMaterializationGuard {
        root: &root,
        byte_cap: 9,
        entry_cap: 10,
    })
    .expect("materialization observation");
    assert_eq!(
        bytes,
        Some(CloneMaterializationCap::Bytes {
            observed: 10,
            cap: 9,
        })
    );
    assert_eq!(
        clone_materialization_cap(CloneMaterializationGuard {
            root: &root,
            byte_cap: 10,
            entry_cap: 1,
        })
        .expect("exact-cap observation"),
        None,
        "materialization exactly at both configured maxima remains valid"
    );

    let entries = clone_materialization_cap(CloneMaterializationGuard {
        root: &root,
        byte_cap: usize::MAX,
        entry_cap: 0,
    })
    .expect("materialization observation");
    assert_eq!(
        entries,
        Some(CloneMaterializationCap::Entries {
            observed: 1,
            cap: 0,
        })
    );
}

#[test]
fn limit_is_a_typed_truncation() {
    let error = clone_materialization_truncated(
        "github",
        "acme/rocket",
        CloneMaterializationCap::Bytes {
            observed: 11,
            cap: 10,
        },
        None,
    );
    match error {
        SourceError::Coverage {
            adapter,
            surface,
            target,
            kind,
            detail,
        } => {
            assert_eq!(adapter, "github");
            assert_eq!(surface, "clone");
            assert_eq!(target, "acme/rocket");
            assert_eq!(kind, SourceCoverageGapKind::Truncated);
            assert!(
                detail.contains("git_total_bytes")
                    && detail.contains("stopped")
                    && detail.contains("not scanned"),
                "truncation must explain the cap and incomplete coverage: {detail}"
            );
        }
        other => panic!("clone materialization cap must stay typed, got {other}"),
    }
}

#[cfg(unix)]
#[test]
fn guard_does_not_follow_symlinks() {
    use std::os::unix::fs::symlink;

    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("clone");
    std::fs::create_dir(&root).expect("clone root");
    let outside = temp.path().join("outside");
    std::fs::write(&outside, vec![b'x'; 4096]).expect("outside payload");
    symlink(&outside, root.join("link")).expect("symlink");

    let cap = outside.as_os_str().len() + 1;
    let result = clone_materialization_cap(CloneMaterializationGuard {
        root: &root,
        byte_cap: cap,
        entry_cap: 1,
    })
    .expect("materialization observation");
    assert_eq!(
        result, None,
        "the guard must count the symlink itself without traversing its target"
    );
}

#[cfg(unix)]
#[test]
fn cap_kills_and_reaps_the_child() {
    use std::process::Stdio;
    use std::time::{Duration, Instant};

    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("clone");
    std::fs::create_dir(&root).expect("clone root");
    let child = std::process::Command::new("/bin/sh")
        .arg("-c")
        .arg("printf 0123456789 > \"$1/payload\"; while :; do :; done")
        .arg("keyhog-materialization-fixture")
        .arg(&root)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("fixture child");
    let child_id = child.id();
    let started = Instant::now();
    let result = super::wait_for_command_with_timeout(
        child,
        None,
        None,
        Duration::from_secs(5),
        CloneMaterializationGuard {
            root: &root,
            byte_cap: 9,
            entry_cap: 10,
        },
    );
    let error = match result {
        Err(error) => error,
        Ok(_) => panic!("oversized materialization must stop the clone child"),
    };

    assert!(
        matches!(
            error,
            super::HostedGitWaitError::MaterializationCap {
                cap: CloneMaterializationCap::Bytes {
                    observed: 10,
                    cap: 9
                },
                cleanup_error: None,
            }
        ),
        "unexpected wait error: {error:?}"
    );
    assert!(
        started.elapsed() < Duration::from_secs(5),
        "materialization guard must stop the child before the timeout"
    );
    if std::path::Path::new("/proc").is_dir() {
        assert!(
            !std::path::PathBuf::from(format!("/proc/{child_id}")).exists(),
            "materialization guard returned before child {child_id} was reaped"
        );
    }
}