crates/mimir-room/tests/room/common.rs
//! A small hand-built log, written through mimir-ingest's own consumer (a
//! scratch git repo + a scripted dumper emitting real mimir-schema rows), so
//! the room reads exactly what production writes.
//!
//! repo `room` (crate k, binary layer ON), universe crate `dep@1.0.0`:
//!
//! ```text
//! seq 1 a→b b→c c→x(ffi) a→dep h(stub) CALLS d→T USES_TYPE
//! I→T FOR_TYPE I→tr IMPLEMENTS
//! binary: a, b, dep h linked; k t<u32> INSTANCE_OF k t; extern write
//! seq 2 + e + e→a + e→b − c→x
//! seq 3 − d (and d→T) + b→T USES_TYPE binary no longer links b
//! k c's type changes (payload only) + k DEPENDS_ON dep2 (new ring crate)
//! every seq: crate k DEPENDS_ON dep · I CONTAINS m (method) · u CALLS m
//! universe: dep h → dep w(ffi) CALLS; dep2 (dumped at seq 3): dep2 z;
//! rt (seq 1): rt unwind, linked by the binary, no DEPENDS_ON names rt
//! ```
//!
//! repo `plain` (crate p, NO binary stream), seq 1: p1→p2, p2→k a (stub) CALLS.
use std::collections::BTreeSet;
use std::path::Path;
use mimir_ingest::{BinaryDump, CommitDump, Consumer, CrateDump, Dumper, EventLog, RepoSpec, Specs};
use mimir_schema::{EdgeKind, EdgeRow, Level, NodeRow, Origin, Provenance, SymbolRow, role};
pub const ROOM: &str = "room";
pub const PLAIN: &str = "plain";
pub const ARTIFACT: &str = "k@x86_64-fixture:dev";
pub fn node(id: &str, kind: &str, krate: &str, origin: Origin, defined: bool) -> NodeRow {
let version = if krate.starts_with("dep") { "1.0.0" } else { "0.1.0" };
let mut row = NodeRow::new(id, kind, Level::Item, id.rsplit(' ').next().unwrap_or(id), krate, version, origin);
row.defined_here = defined;
row
}
pub fn k(id: &str, kind: &str) -> NodeRow {
node(&format!("k {id}"), kind, "k", Origin::Local, true)
}
pub fn edge(from: &str, to: &str, kind: EdgeKind) -> EdgeRow {
EdgeRow::new(from, to, kind)
}
fn sym(id: &str, symbol: &str, r: &str, krate: &str) -> SymbolRow {
SymbolRow {
id: id.into(),
symbol: symbol.into(),
demangled: None,
role: r.into(),
krate: krate.into(),
version: "0.1.0".into(),
binary: Some("k".into()),
section: (r != role::IMPORT).then(|| ".text".into()),
addr: (r != role::IMPORT).then_some(0x1000),
size: (r != role::IMPORT).then_some(16),
provenance: Provenance::BinaryOnly,
valid_from_sha: None,
valid_to_sha: None,
}
}
/// One commit's dump: local nodes + edges of crate `owner`, binary rows.
pub struct Step {
pub owner: &'static str,
pub nodes: Vec<NodeRow>,
pub edges: Vec<EdgeRow>,
pub binary: Option<BinaryDump>,
/// Ring crates this commit dumps besides `dep@1.0.0` (owner, nodes, edges).
pub ring: Vec<(&'static str, Vec<NodeRow>, Vec<EdgeRow>)>,
}
pub struct Scripted(pub Vec<Step>);
impl Dumper for Scripted {
fn dump(&mut self, _: &Path, _: &str) -> anyhow::Result<CommitDump> {
let s = self.0.remove(0);
let pkg = s.owner.split('@').next().unwrap();
let mut targets: BTreeSet<String> = [format!("{pkg}:lib")].into();
if let Some(b) = &s.binary {
// the artifact's bin is declared (else the consumer closes it)
targets.insert(format!("{pkg}:bin:{}", BinaryDump::name_of(&b.artifact)));
}
let universe = CrateDump {
krate: "dep@1.0.0:lib".into(),
universe: true,
nodes: vec![mimir_schema::nodes_batch(&[
node("dep", "crate", "dep", Origin::Library, true),
node("dep h", "fn", "dep", Origin::Library, true),
node("dep w", "ffi", "dep", Origin::Library, true),
])?],
edges: vec![mimir_schema::edges_batch(&[edge("dep h", "dep w", EdgeKind::Calls)])?],
spans: vec![],
};
Ok(CommitDump {
toolchain: "nightly-fixture".into(),
workspace_crates: [s.owner.to_string()].into(),
workspace_targets: targets,
failed_units: BTreeSet::new(),
owner_disambiguations: 0,
crates: [
vec![
CrateDump {
krate: s.owner.into(),
universe: false,
nodes: vec![mimir_schema::nodes_batch(&s.nodes)?],
edges: vec![mimir_schema::edges_batch(&s.edges)?],
spans: vec![],
},
universe,
],
s.ring
.iter()
.map(|(owner, n, e)| CrateDump {
krate: owner.to_string(),
universe: true,
nodes: vec![mimir_schema::nodes_batch(n).unwrap()],
edges: vec![mimir_schema::edges_batch(e).unwrap()],
spans: vec![],
})
.collect(),
]
.concat(),
binary: s.binary.is_some(),
binaries: s.binary.into_iter().collect(),
})
}
/// A binary-only re-ingest (the artifact relinked): links `b` again.
fn binaries(&mut self, _: &Path, _: &str) -> anyhow::Result<Vec<BinaryDump>> {
Ok(vec![binary(true)])
}
}
fn binary(link_b: bool) -> BinaryDump {
let mut syms = vec![
sym("k a", "_Rka", role::ITEM, "k"),
sym("dep h", "_Rdeph", role::ITEM, "dep"),
sym("k t<u32>", "_Rktu32", role::MONO, "k"),
sym("extern write", "write", role::IMPORT, "extern"),
// a ring crate the compiler links though nothing DEPENDS_ON it (a panic runtime)
sym("rt unwind", "_Rrtunwind", role::ITEM, "rt"),
];
if link_b {
syms.push(sym("k b", "_Rkb", role::ITEM, "k"));
}
let mut mono = node("k t<u32>", "mono_instance", "k", Origin::Local, true);
mono.provenance = Provenance::BinaryOnly;
let mut imp = node("extern write", "ffi_import", "extern", Origin::Library, true);
imp.provenance = Provenance::BinaryOnly;
let mut inst = edge("k t<u32>", "k t", EdgeKind::InstanceOf);
inst.provenance = Provenance::BinaryOnly;
BinaryDump {
artifact: ARTIFACT.into(),
nodes: vec![mimir_schema::nodes_batch(&[mono, imp]).unwrap()],
edges: vec![mimir_schema::edges_batch(&[inst]).unwrap()],
symbols: vec![mimir_schema::symbols_batch(&syms).unwrap()],
ledger: None,
ms: 0,
}
}
pub fn room_steps() -> Vec<Step> {
use EdgeKind::*;
let base = || {
vec![
k("a", "fn"),
k("b", "fn"),
k("c", "fn"),
k("t", "fn"),
k("T", "struct"),
k("I", "impl"),
k("tr", "trait"),
k("x", "ffi"),
k("m", "method"),
k("u", "fn"),
node("k", "crate", "k", Origin::Local, true),
node("dep", "crate", "dep", Origin::Library, false),
node("dep h", "fn", "dep", Origin::Library, false),
]
};
let e = |f: &str, t: &str, kd| edge(&format!("k {f}"), &if t.starts_with("dep ") { t.to_string() } else { format!("k {t}") }, kd);
let s1_nodes = [base(), vec![k("d", "fn")]].concat();
let s1_edges = vec![
e("a", "b", Calls),
e("b", "c", Calls),
e("c", "x", Calls),
e("a", "dep h", Calls),
e("d", "T", UsesType),
e("I", "T", ForType),
e("I", "tr", Implements),
e("I", "m", Contains),
e("u", "m", Calls),
edge("k", "dep", DependsOn),
];
let s2_nodes = [base(), vec![k("d", "fn"), k("e", "fn")]].concat();
let mut s2_edges: Vec<EdgeRow> = s1_edges.iter().filter(|x| !(x.from == "k c" && x.to == "k x")).cloned().collect();
s2_edges.extend([e("e", "a", Calls), e("e", "b", Calls)]);
let mut c3 = k("c", "fn");
c3.ty = Some("fn() -> u64".into());
let s3_nodes = [base().into_iter().filter(|n| n.id != "k c").collect(), vec![c3, k("e", "fn"), node("dep2", "crate", "dep2", Origin::Library, false)]].concat();
let mut s3_edges: Vec<EdgeRow> = s2_edges.iter().filter(|x| x.from != "k d").cloned().collect();
s3_edges.push(e("b", "T", UsesType));
s3_edges.push(edge("k", "dep2", DependsOn));
let rt = ("rt@1.0.0:lib", vec![node("rt", "crate", "rt", Origin::Lang, true), node("rt unwind", "fn", "rt", Origin::Lang, true)], vec![]);
let dep2 = ("dep2@1.0.0:lib", vec![node("dep2", "crate", "dep2", Origin::Library, true), node("dep2 z", "fn", "dep2", Origin::Library, true)], vec![]);
vec![
Step { owner: "k@0.1.0:lib", nodes: s1_nodes, edges: s1_edges, binary: Some(binary(true)), ring: vec![rt] },
Step { owner: "k@0.1.0:lib", nodes: s2_nodes, edges: s2_edges, binary: Some(binary(true)), ring: vec![] },
Step { owner: "k@0.1.0:lib", nodes: s3_nodes, edges: s3_edges, binary: Some(binary(false)), ring: vec![dep2] },
]
}
pub fn plain_steps() -> Vec<Step> {
vec![Step {
owner: "p@0.1.0:lib",
nodes: vec![
node("p p1", "fn", "p", Origin::Local, true),
node("p p2", "fn", "p", Origin::Local, true),
node("k a", "fn", "k", Origin::Library, false),
],
edges: vec![edge("p p1", "p p2", EdgeKind::Calls), edge("p p2", "k a", EdgeKind::Calls)],
binary: None,
ring: vec![],
}]
}
/// A scratch repo with `n` commits that each touch a `.rs` file.
pub fn scratch(dir: &Path, n: usize) {
nornir_git::gitio::init(dir).unwrap();
for i in 0..n {
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("src/lib.rs"), format!("// commit {i}\n")).unwrap();
nornir_git::gitio::commit_all(dir, &format!("c{i}")).unwrap();
}
}
pub fn repo_spec(tmp: &Path, name: &str) -> RepoSpec {
RepoSpec {
name: name.into(),
source: tmp.join(format!("src-{name}")).to_string_lossy().into_owned(),
branch: "main".into(),
worktree: tmp.join(format!("clone-{name}")),
initial_offset: None,
}
}
pub async fn open_log(tmp: &Path) -> EventLog {
let wh = skade::open(tmp.join("lake")).await.unwrap();
EventLog::open(wh, Specs::mimir().unwrap()).await.unwrap()
}
/// Consume `steps` for repo `name` (its scratch repo gets one commit per step).
pub async fn consume(log: EventLog, tmp: &Path, name: &str, steps: Vec<Step>) -> EventLog {
scratch(&tmp.join(format!("src-{name}")), steps.len());
let mut c = Consumer::boot(log, Scripted(steps)).await.unwrap();
let rep = c.run(&repo_spec(tmp, name)).await.unwrap();
assert!(rep.iter().all(|r| r.status == "ok"), "{rep:?}");
c.into_parts().0
}
/// The whole fixture: `room` seq 1..3, `plain` seq 1.
pub async fn fixture(tmp: &Path) -> EventLog {
let log = open_log(tmp).await;
let log = consume(log, tmp, ROOM, room_steps()).await;
consume(log, tmp, PLAIN, plain_steps()).await
}
/// The room cache is per process: tests that open rooms run one at a time so
/// the cache test sees only its own entries.
static SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
pub async fn serial() -> tokio::sync::MutexGuard<'static, ()> {
SERIAL.lock().await
}