crates/dwarves-embed/tests/proto_parity.rs
//! **The trimmed proto really is a trim, not a fork.**
//!
//! `proto/dwarf.proto`'s own header states the contract:
//!
//! > Field numbers, message names, and the `dwarf.v1` package MUST match
//! > `crates/dwarves/proto/dwarf.proto` exactly
//!
//! …and until this file, *nothing enforced it*. The two copies are hand-maintained
//! and compiled by three separate `build.rs` (`dwarves`, `dwarves-embed`, and
//! `dwarves-web-client`, which generates from the EMBED copy) — so the day someone
//! renumbers a field in the server proto, the embed pane and the wasm grpc-web
//! client silently mis-decode a live `dwarves-server`. Silently, because protobuf
//! decodes by NUMBER: a field whose number moved does not error, it lands in the
//! wrong slot or vanishes.
//!
//! This is a drift TRAP rather than a live break — at the time of writing every
//! shared message agrees — which is exactly why it needs a guard: a trap nobody has
//! stepped in yet looks identical to a trap that is not there.
//!
//! The check is deliberately a small hand-rolled parser over the two `.proto` texts
//! rather than a descriptor diff: it must run with no build-script output, no
//! protoc, and no dev-dependency, so it can never be the reason CI is skipped.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
/// `message/enum name -> { field name -> field number }`, flattened to the LEAF
/// name (nested `Foo.Bar` is keyed as `Bar`) because the wire only cares about the
/// message a field belongs to, not the nesting path used to spell it.
type Schema = BTreeMap<String, BTreeMap<String, u32>>;
/// `service name -> { rpc name -> (request, response) }`, types leaf-normalised.
type Services = BTreeMap<String, BTreeMap<String, (String, String)>>;
fn strip_comments(src: &str) -> String {
// Block comments first, then line comments — proto has no string literal that
// can contain `//` in the constructs we parse, so this is sufficient here.
let mut out = String::with_capacity(src.len());
let mut rest = src;
while let Some(i) = rest.find("/*") {
out.push_str(&rest[..i]);
match rest[i + 2..].find("*/") {
Some(j) => rest = &rest[i + 2 + j + 2..],
None => {
rest = "";
break;
}
}
}
out.push_str(rest);
out.lines()
.map(|l| l.split("//").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n")
}
/// Pull `<keyword> <Name> {` out of a line, if present.
fn open_block(line: &str) -> Option<(&'static str, String)> {
for kw in ["message", "enum", "service", "oneof"] {
let mut it = line.split_whitespace();
// the keyword may be preceded only by whitespace
if it.next() == Some(kw) {
if let Some(name) = it.next() {
let name = name.trim_end_matches('{').trim();
if !name.is_empty() && line.contains('{') {
let kw: &'static str = match kw {
"message" => "message",
"enum" => "enum",
"service" => "service",
_ => "oneof",
};
return Some((kw, name.to_string()));
}
}
}
}
None
}
/// `<type> <name> = <n>;` (also matches enum values `<NAME> = <n>;`).
fn field(line: &str) -> Option<(String, u32)> {
let line = line.trim().trim_end_matches(';');
let (lhs, rhs) = line.split_once('=')?;
// `[deprecated = true]`-style options trail the number; take the leading digits.
let num: String = rhs
.trim()
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if num.is_empty() {
return None;
}
let name = lhs.split_whitespace().last()?;
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|| name.chars().next()?.is_ascii_digit()
{
return None;
}
Some((name.to_string(), num.parse().ok()?))
}
fn rpc(line: &str) -> Option<(String, String, String)> {
let l = line.trim();
let rest = l.strip_prefix("rpc ")?;
let (name, rest) = rest.split_once('(')?;
let (req, rest) = rest.split_once(')')?;
let rest = rest.trim_start().strip_prefix("returns")?;
let (_, rest) = rest.split_once('(')?;
let (resp, _) = rest.split_once(')')?;
let leaf = |s: &str| {
s.trim()
.trim_start_matches("stream ")
.trim()
.rsplit('.')
.next()
.unwrap_or("")
.to_string()
};
Some((name.trim().to_string(), leaf(req), leaf(resp)))
}
fn parse(path: &Path) -> (Schema, Services) {
let src = strip_comments(&std::fs::read_to_string(path).unwrap_or_else(|e| {
panic!("read {}: {e}", path.display());
}));
let mut schema = Schema::new();
let mut services = Services::new();
// (kind, name, brace depth at which the block was opened)
let mut stack: Vec<(&'static str, String, i32)> = Vec::new();
let mut depth: i32 = 0;
for line in src.lines() {
let opened = open_block(line.trim());
if let Some((kind, ref name)) = opened {
match kind {
"message" | "enum" => {
schema.entry(name.clone()).or_default();
}
"service" => {
services.entry(name.clone()).or_default();
}
_ => {} // oneof is transparent: its fields belong to the parent
}
stack.push((kind, name.clone(), depth));
} else {
// Fields belong to the innermost enclosing message/enum.
if let Some((_, owner, _)) = stack
.iter()
.rev()
.find(|(k, _, _)| *k == "message" || *k == "enum")
{
if let Some((fname, num)) = field(line) {
schema.entry(owner.clone()).or_default().insert(fname, num);
}
}
if let Some((kind, svc, _)) = stack.last() {
if *kind == "service" {
if let Some((n, req, resp)) = rpc(line) {
services
.entry(svc.clone())
.or_default()
.insert(n, (req, resp));
}
}
}
}
for c in line.chars() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
while stack.last().map(|(_, _, d)| *d >= depth).unwrap_or(false) {
stack.pop();
}
}
_ => {}
}
}
}
(schema, services)
}
fn repo_root() -> PathBuf {
// crates/dwarves-embed -> crates -> <repo>
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("dwarves-embed sits at <repo>/crates/dwarves-embed")
.to_path_buf()
}
fn server_proto() -> PathBuf {
repo_root().join("crates/dwarves/proto/dwarf.proto")
}
fn embed_proto() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("proto/dwarf.proto")
}
/// Both files must exist. A guard whose inputs silently vanish is worse than no
/// guard — it would report green forever (the exact class this repo keeps hitting).
#[test]
fn both_protos_are_present_and_parse_to_something() {
for p in [server_proto(), embed_proto()] {
assert!(
p.is_file(),
"proto missing: {} — this test cannot be satisfied by an absent input",
p.display()
);
let (schema, services) = parse(&p);
assert!(
!schema.is_empty() && !services.is_empty(),
"{} parsed to {} message(s) / {} service(s) — the parser or the file is broken; \
an empty parse must never be mistaken for agreement",
p.display(),
schema.len(),
services.len()
);
}
}
/// THE CONTRACT: every message the trimmed copy re-declares must use the SAME field
/// numbers as the server proto, and must not introduce a field the server does not
/// have. Fields the embed copy omits are fine — that is what "trimmed" means.
#[test]
fn embed_field_numbers_match_the_server_proto_exactly() {
let (server, _) = parse(&server_proto());
let (embed, _) = parse(&embed_proto());
let mut problems: Vec<String> = Vec::new();
let mut checked_messages = 0usize;
let mut checked_fields = 0usize;
for (msg, efields) in &embed {
let Some(sfields) = server.get(msg) else {
problems.push(format!(
"message `{msg}` exists in the EMBED copy but NOT in the server proto \
— the trim has forked (add it to the server proto or drop it here)"
));
continue;
};
checked_messages += 1;
for (fname, enum_) in efields {
checked_fields += 1;
match sfields.get(fname) {
Some(snum) if snum == enum_ => {}
Some(snum) => problems.push(format!(
"FIELD-NUMBER CONFLICT {msg}.{fname}: server={snum} embed={enum_} \
— protobuf decodes by NUMBER, so a live server and this client \
would silently disagree about this field"
)),
None => problems.push(format!(
"field `{msg}.{fname}` (= {enum_}) exists only in the EMBED copy \
— the server will never send it and never accept it"
)),
}
}
// A number reused for a DIFFERENT name is the nastiest form: both sides
// parse happily and the value lands in the wrong field.
for (fname, enum_) in efields {
if let Some((other, _)) = sfields.iter().find(|(n, v)| *v == enum_ && *n != fname) {
problems.push(format!(
"NUMBER {enum_} REUSED: server calls it `{msg}.{other}`, embed calls it \
`{msg}.{fname}` — a decode would silently cross the wires"
));
}
}
}
assert!(
checked_messages > 0 && checked_fields > 0,
"the guard compared {checked_messages} message(s) / {checked_fields} field(s) — \
it must never pass vacuously; if the trim legitimately shares no message with \
the server proto, this test has lost its subject"
);
assert!(
problems.is_empty(),
"the trimmed `dwarves-embed/proto/dwarf.proto` has DRIFTED from \
`dwarves/proto/dwarf.proto` (its own header says they MUST match exactly).\n\
NOTE `dwarves-web-client` generates from the EMBED copy, so a drift here also \
breaks the wasm grpc-web client.\n - {}",
problems.join("\n - ")
);
eprintln!("proto parity: {checked_messages} shared message(s), {checked_fields} field(s) — all numbers agree");
}
/// The trimmed copy may serve FEWER RPCs, but the ones it declares must have the
/// same request/response types — the gRPC method path is `/dwarf.v1.Dwarf/<Rpc>`,
/// so a mismatched body type is a runtime decode failure, not a compile error.
#[test]
fn embed_rpcs_are_a_subset_with_identical_signatures() {
let (_, server) = parse(&server_proto());
let (_, embed) = parse(&embed_proto());
let mut problems: Vec<String> = Vec::new();
let mut checked = 0usize;
for (svc, erpcs) in &embed {
let Some(srpcs) = server.get(svc) else {
problems.push(format!("service `{svc}` exists only in the EMBED copy"));
continue;
};
for (name, esig) in erpcs {
checked += 1;
match srpcs.get(name) {
Some(ssig) if ssig == esig => {}
Some(ssig) => problems.push(format!(
"RPC SIGNATURE DIFFERS {svc}/{name}: server={ssig:?} embed={esig:?}"
)),
None => problems.push(format!(
"RPC `{svc}/{name}` exists only in the EMBED copy — a live server \
would answer UNIMPLEMENTED"
)),
}
}
}
assert!(
checked > 0,
"no RPCs compared — the guard has lost its subject"
);
assert!(
problems.is_empty(),
"embed RPC drift:\n - {}",
problems.join("\n - ")
);
}
/// The gRPC method path embeds the package, so the two files must agree on it too.
#[test]
fn both_protos_declare_the_same_package() {
let pkg = |p: &Path| -> String {
strip_comments(&std::fs::read_to_string(p).unwrap())
.lines()
.find_map(|l| {
l.trim()
.strip_prefix("package ")
.map(|s| s.trim().trim_end_matches(';').to_string())
})
.unwrap_or_else(|| panic!("{} declares no package", p.display()))
};
let (a, b) = (pkg(&server_proto()), pkg(&embed_proto()));
assert_eq!(
a, b,
"package mismatch: the gRPC method path is `/<package>.Dwarf/<Rpc>`, so a \
differing package means EVERY call 404s as UNIMPLEMENTED"
);
}
// ── THE THIRD COPY (2026-08-24) ───────────────────────────────────────────────
//
// `dwarves-web-client` used to have NO copy: its `build.rs` read
// `../dwarves-embed/proto/dwarf.proto` straight out of this crate's directory. That is
// real reuse inside the checkout and a broken crate on crates.io — `cargo package` only
// tarballs files from under the package root, so `../` is simply absent from the
// published crate and its build script panicked:
//
// protox: compile dwarf.proto: file '../dwarves-embed/proto/dwarf.proto'
// is not in any include path
//
// So the wasm client now owns `crates/dwarves-web-client/proto/dwarf.proto` like the
// other two own theirs — and this file grows the guard to match, because the module docs
// of `dwarves-web-client` still promise the generated types are *byte-identical* to the
// native client's, and after the copy nothing but a test can keep that true.
//
// The web copy is held to a STRICTER rule than the embed copy: not "a trim with matching
// numbers" but BYTE-IDENTICAL to the embed copy. It was produced by `cp`, it exists only
// so the file is inside the tarball, and it has no reason to differ by one character.
// A byte compare cannot be argued with and cannot pass vacuously.
fn web_client_proto() -> PathBuf {
repo_root().join("crates/dwarves-web-client/proto/dwarf.proto")
}
/// The web copy must be PRESENT. Its absence is not "no drift", it is a crate whose
/// build script cannot run — which is exactly the defect the copy was made to fix.
#[test]
fn the_web_client_owns_a_proto_inside_its_own_package_root() {
let p = web_client_proto();
assert!(
p.is_file(),
"{} is missing — `dwarves-web-client/build.rs` compiles `proto/dwarf.proto` \
relative to its OWN manifest dir, so without this file the published crate \
cannot build at all (and `cargo package` cannot reach outside the package root \
to find it)",
p.display()
);
let (schema, services) = parse(&p);
assert!(
!schema.is_empty() && !services.is_empty(),
"{} parsed to {} message(s) / {} service(s) — an empty parse must never be \
mistaken for agreement",
p.display(),
schema.len(),
services.len()
);
}
/// BYTE-IDENTICAL to the embed copy. `dwarves-web-client`'s docs claim its `DwarfClient`
/// and every message type are identical to the native client's; the two files being the
/// same bytes is the only thing that still makes that sentence true now that they are
/// two files.
#[test]
fn the_web_client_proto_is_byte_identical_to_the_embed_proto() {
let (embed, web) = (embed_proto(), web_client_proto());
let a = std::fs::read(&embed).unwrap_or_else(|e| panic!("read {}: {e}", embed.display()));
let b = std::fs::read(&web).unwrap_or_else(|e| panic!("read {}: {e}", web.display()));
assert!(
!a.is_empty(),
"{} is EMPTY — this guard would then pass by comparing nothing to nothing",
embed.display()
);
if a != b {
// Name the first differing line, so the failure is actionable rather than
// just "these differ".
let (sa, sb) = (String::from_utf8_lossy(&a), String::from_utf8_lossy(&b));
let first = sa
.lines()
.zip(sb.lines())
.enumerate()
.find(|(_, (x, y))| x != y)
.map(|(i, (x, y))| format!("line {}:\n embed: {x}\n web: {y}", i + 1))
.unwrap_or_else(|| {
format!(
"no differing line in the common prefix — the files differ in LENGTH \
({} vs {} lines)",
sa.lines().count(),
sb.lines().count()
)
});
panic!(
"`dwarves-web-client/proto/dwarf.proto` has DRIFTED from \
`dwarves-embed/proto/dwarf.proto`.\nThey must be byte-identical: the web \
copy exists ONLY so the file lands inside the published tarball, and \
protobuf decodes by NUMBER — a field that moves in one copy and not the \
other does not error, it silently mis-decodes against a live \
dwarves-server.\n{first}\n({} bytes vs {} bytes)",
a.len(),
b.len()
);
}
eprintln!(
"proto parity: dwarves-web-client/proto/dwarf.proto is byte-identical to the \
embed copy ({} bytes)",
a.len()
);
}
/// And the web copy must agree with the SERVER proto on every field number it declares —
/// the same contract the embed copy is held to. Byte-identity with the embed copy already
/// implies this today, but the two facts are independent claims and a future trim of the
/// web copy must not escape the number check by having stopped being a `cp`.
#[test]
fn web_client_field_numbers_match_the_server_proto_exactly() {
let (server, _) = parse(&server_proto());
let (web, _) = parse(&web_client_proto());
let mut problems: Vec<String> = Vec::new();
let mut checked_fields = 0usize;
for (msg, wfields) in &web {
let Some(sfields) = server.get(msg) else {
problems.push(format!(
"message `{msg}` exists in the WEB copy but NOT in the server proto"
));
continue;
};
for (fname, wnum) in wfields {
checked_fields += 1;
match sfields.get(fname) {
Some(snum) if snum == wnum => {}
Some(snum) => problems.push(format!(
"FIELD-NUMBER CONFLICT {msg}.{fname}: server={snum} web={wnum} \
— the browser client would silently mis-decode this field"
)),
None => problems.push(format!(
"field `{msg}.{fname}` (= {wnum}) exists only in the WEB copy"
)),
}
}
}
assert!(
checked_fields > 0,
"the guard compared {checked_fields} field(s) — it must never pass vacuously"
);
assert!(
problems.is_empty(),
"the wasm grpc-web client's proto has DRIFTED from the server proto:\n - {}",
problems.join("\n - ")
);
eprintln!("proto parity: web copy agrees with the server proto on {checked_fields} field(s)");
}