Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-lfs/tests/swallowed_errors.rs

#![cfg(feature = "fs")]

//! **An abandoned LFS upload that will not unlink must produce a line.**
//!
//! `FsPendingWrite::drop` removed the partial file with `let _ =
//! std::fs::remove_file(temp)`. That swallow is not the same as the two the
//! audit already closed in `gunnar-store`: a leaked `.keep` is collected by the
//! next `repack()`, and a leaked repack scratch file is collected by
//! `sweep_stale_scratch`. **Nothing in the tree ever looks at
//! `lfs/incomplete/`.** So a rejected or severed upload whose temp file cannot
//! be removed stays on disk at full size, under a name no oid resolves to, and
//! nothing anywhere says so.
//!
//! The failure below is a real `EACCES` from the real filesystem — a marker
//! inside a directory made read-only — rather than a stub, because the error
//! being asserted about is the one the production path actually discarded.

use std::io::Write as _;
use std::sync::{Arc, Mutex};

use gunnar_lfs::fs_store::FsLfsStore;
use gunnar_lfs::store::LfsStore;
use gunnar_lfs::Oid;
use tracing::subscriber::with_default;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Layer;

/// Every event seen, as `target` plus the rendered fields.
#[derive(Clone, Default)]
struct Capture(Arc<Mutex<Vec<(String, String)>>>);

struct CaptureLayer(Capture);

impl<S: tracing::Subscriber> Layer<S> for CaptureLayer {
    fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
        struct Render(String);
        impl tracing::field::Visit for Render {
            fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
                self.0.push_str(&format!("{}={value:?} ", field.name()));
            }
        }
        let mut render = Render(String::new());
        event.record(&mut render);
        self.0
             .0
            .lock()
            .expect("the capture lock")
            .push((event.metadata().target().to_owned(), render.0));
    }
}

fn capturing<T>(body: impl FnOnce() -> T) -> (T, Vec<(String, String)>) {
    let capture = Capture::default();
    let subscriber = tracing_subscriber::registry().with(CaptureLayer(capture.clone()));
    let out = with_default(subscriber, body);
    let events = capture.0.lock().expect("the capture lock").clone();
    (out, events)
}

/// An oid whose value does not matter: the upload is abandoned, never committed.
fn some_oid() -> Oid {
    Oid::parse("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
        .expect("a valid sha256 oid")
}

#[test]
fn an_abandoned_upload_that_cannot_be_removed_is_reported() {
    let dir = tempfile::tempdir().expect("temporary directory");
    let store = FsLfsStore::open_or_init(dir.path().join("repo.git")).expect("store");

    let incomplete = dir.path().join("repo.git").join("lfs").join("incomplete");
    let normal = std::fs::metadata(&incomplete)
        .expect("the incomplete directory")
        .permissions();

    let (temp_files, events) = capturing(|| {
        let mut pending = store.begin_write(&some_oid(), 8).expect("begin_write");
        pending.write_all(b"half of a").expect("write");
        // The upload is now on disk under `incomplete/`. Make the directory
        // read-only so the unlink in `Drop` gets a genuine EACCES.
        let mut locked = normal.clone();
        locked.set_readonly(true);
        std::fs::set_permissions(&incomplete, locked).expect("make the directory read-only");
        drop(pending);
        std::fs::read_dir(&incomplete)
            .expect("read incomplete/")
            .flatten()
            .count()
    });

    // Undo before any assertion, or a failure leaves the tempdir undeletable.
    std::fs::set_permissions(&incomplete, normal).expect("restore permissions");

    // The premise, asserted rather than assumed: the file really did survive.
    // Without this the test would pass on a machine where the removal succeeded
    // anyway (root, or a filesystem that ignores the mode), having proved
    // nothing about the missing log line.
    assert_eq!(
        temp_files, 1,
        "the abandoned upload was removed after all, so no error was possible and this test \
         cannot tell a reported failure from a silent one. Almost always: the suite is \
         running as ROOT, which ignores the directory mode this injection depends on — in \
         the toolchain container, `podman run --userns=keep-id`."
    );

    let reported: Vec<_> = events
        .iter()
        .filter(|(target, fields)| {
            target == gunnar_lfs::LFS_TARGET && fields.contains("incomplete")
        })
        .collect();
    assert_eq!(
        reported.len(),
        1,
        "an abandoned LFS upload that could not be removed produced {} lines, not one. All \
         events: {events:?}",
        reported.len()
    );
}

/// The other side: the ordinary abandoned upload is **silent** and is really
/// gone.
///
/// Without this, a `drop_incomplete` that logged unconditionally would satisfy
/// the test above and put an error line in an operator's journal on every
/// cancelled upload.
#[test]
fn an_abandoned_upload_that_can_be_removed_is_silent_and_actually_goes() {
    let dir = tempfile::tempdir().expect("temporary directory");
    let store = FsLfsStore::open_or_init(dir.path().join("repo.git")).expect("store");
    let incomplete = dir.path().join("repo.git").join("lfs").join("incomplete");

    let (left_behind, events) = capturing(|| {
        let mut pending = store.begin_write(&some_oid(), 8).expect("begin_write");
        pending.write_all(b"half of a").expect("write");
        drop(pending);
        std::fs::read_dir(&incomplete)
            .expect("read incomplete/")
            .flatten()
            .count()
    });

    assert_eq!(left_behind, 0, "a dropped upload left a file behind");
    assert!(
        events.is_empty(),
        "a cancelled upload is the ordinary case and must be silent, but it logged: {events:?}"
    );
}