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

nordisk/nornir

crates/mimir-room/tests/room/live.rs

//! A room opened WHILE the consumer writes the same log (wiring map R13), a
//! room from a sync caller (R14), and the typed "never consumed" (R15).
//!
//! The consumer is held inside a commit by a gated dumper — where a real
//! consume spends its minutes (`cargo check`) — on its own thread; rooms are
//! opened from the test thread over a clone of the same log handle.

use std::path::Path;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;

use anyhow::Result;
use mimir_ingest::{CommitDump, Consumer, Dumper};
use mimir_room::{Room, Scope, ScopeError};
use mimir_schema::EdgeKind;

use super::common::*;

const WAIT: Duration = Duration::from_secs(60);

/// The scripted dumper, stopping inside its `hold_at`-th dump until released.
struct Gated {
    inner: Scripted,
    calls: usize,
    hold_at: usize,
    entered: Sender<()>,
    go: Receiver<()>,
}

impl Dumper for Gated {
    fn dump(&mut self, worktree: &Path, sha: &str) -> Result<CommitDump> {
        self.calls += 1;
        if self.calls == self.hold_at {
            self.entered.send(()).unwrap();
            self.go.recv_timeout(WAIT).expect("the test never released the gated dump");
        }
        self.inner.dump(worktree, sha)
    }
}

/// `Room::open_blocking` on a thread of its own, so a blocked open is a test
/// failure with a reason instead of a hung test.
fn open_within(log: &mimir_ingest::EventLog, scope: Scope, what: &str) -> mimir_room::Opened {
    let (tx, rx) = channel();
    let log = log.clone();
    std::thread::spawn(move || tx.send(Room::open_blocking(&log, &scope)).ok());
    rx.recv_timeout(Duration::from_secs(20))
        .unwrap_or_else(|_| panic!("{what}: the room did not open within 20 s — a reader is waiting for the writer"))
        .unwrap_or_else(|e| panic!("{what}: {e:#}"))
}

#[test]
fn a_room_opened_during_an_in_flight_consume_is_the_last_committed_release() {
    let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
    let _g = rt.block_on(serial());
    Room::clear_cache();
    let tmp = tempfile::tempdir().unwrap();
    let log = rt.block_on(open_log(tmp.path()));
    let reader = log.clone();
    let steps = room_steps();
    assert_eq!(steps.len(), 3);
    scratch(&tmp.path().join(format!("src-{ROOM}")), steps.len());
    let spec = repo_spec(tmp.path(), ROOM);
    let (entered_tx, entered) = channel();
    let (go, go_rx) = channel();

    // the consumer: held inside the dump of commit 3
    let writer = std::thread::spawn(move || {
        tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
            let gated = Gated { inner: Scripted(steps), calls: 0, hold_at: 3, entered: entered_tx, go: go_rx };
            let mut c = Consumer::boot(log, gated).await.unwrap();
            c.run(&spec).await.unwrap().len()
        })
    });
    entered.recv_timeout(WAIT).expect("the consumer never reached commit 3");
    assert!(reader.has_writer(), "the consume is in flight");

    // ---- the room opens NOW, and it is release 2: e→a is in, c→x(ffi) is out
    let during = open_within(&reader, Scope::current(), "during the consume");
    assert!(!during.cached);
    assert_eq!(during.room.scope().repos[ROOM], 2, "the last committed release, not the one in flight");
    let callers = during.room.dependents_by(&["k a"], &[EdgeKind::Calls], None, None).unwrap();
    assert!(during.room.monikers(&callers.value.reached).contains(&"k e"), "seq 2 added e→a");
    assert!(during.room.ordinal("k d").is_some(), "seq 3 (in flight) removes d: not yet");
    // a second reader at the same time gets the same snapshot from the cache
    let second = open_within(&reader, Scope::current(), "a second reader during the consume");
    assert!(second.cached && second.room.same_snapshot(&during.room));

    go.send(()).unwrap();
    assert_eq!(writer.join().unwrap(), 3);

    // ---- the next open is release 3; the room held from before did not move
    let after = open_within(&reader, Scope::current(), "after the consume");
    assert!(!after.cached && !after.room.same_snapshot(&during.room));
    assert_eq!(after.room.scope().repos[ROOM], 3);
    assert!(after.room.ordinal("k d").is_none(), "seq 3 removed d");
    assert_eq!(during.room.scope().repos[ROOM], 2);
    assert!(during.room.ordinal("k d").is_some(), "a room is an immutable snapshot");
}

#[test]
fn a_repo_that_was_never_consumed_is_a_typed_scope_error() {
    let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
    let _g = rt.block_on(serial());
    let tmp = tempfile::tempdir().unwrap();
    let log = rt.block_on(fixture(tmp.path()));

    // sync entry (R14), from a thread with no runtime
    let room = Room::at_blocking(&log, &Scope::repo(PLAIN, None)).unwrap();
    assert_eq!(room.scope().repos[PLAIN], 1);
    // … and from inside a runtime's blocking pool
    let l2 = log.clone();
    let n = rt.block_on(async move { tokio::task::spawn_blocking(move || Room::at_blocking(&l2, &Scope::current()).map(|r| r.len())).await.unwrap() }).unwrap();
    assert!(n > 0);

    // R15: typed, not a string to grep
    let err = Room::at_blocking(&log, &Scope::repo("nobody", None)).err().expect("never consumed");
    match err.downcast_ref::<ScopeError>() {
        Some(ScopeError::NotConsumed { repo, known }) => {
            assert_eq!(repo, "nobody");
            assert_eq!(known, &vec![PLAIN.to_string(), ROOM.to_string()]);
        }
        other => panic!("expected ScopeError::NotConsumed, got {other:?} ({err:#})"),
    }
    let past = Room::at_blocking(&log, &Scope::repo(PLAIN, Some(9))).err().unwrap();
    assert!(matches!(past.downcast_ref::<ScopeError>(), Some(ScopeError::PastOffset { seq: 9, offset: 1, .. })), "{past:#}");
}