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

nordisk/dwarves

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)");
}