use std::path::{Path, PathBuf};
use std::process::Command;
#[path = "../src/protocraft/mod.rs"]
mod protocraft;
use protocraft::craft_a;
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
}
fn index_schema(name: &str) -> Option<(String, String)> {
let index_path = repo_root().join("fixtures/index.toml");
let text = std::fs::read_to_string(&index_path)
.unwrap_or_else(|e| panic!("cannot read {}: {}", index_path.display(), e));
let doc: toml::Value = text
.parse()
.unwrap_or_else(|e| panic!("cannot parse index.toml: {e}"));
doc.get("fixture")
.and_then(|v| v.as_array())
.unwrap_or(&vec![])
.iter()
.find(|entry| entry["name"].as_str() == Some(name))
.map(|entry| {
(
entry["schema"].as_str().unwrap().to_owned(),
entry["message"].as_str().unwrap().to_owned(),
)
})
}
fn schema_path(schema_rel: &str) -> PathBuf {
let generated = ["descriptor.pb", "knife.pb", "enum_collision.pb"];
if let Some(name) = generated
.iter()
.find(|&&n| schema_rel == format!("fixtures/schemas/{n}"))
{
return PathBuf::from(env!("OUT_DIR")).join(name);
}
repo_root().join(schema_rel)
}
fn cli_roundtrip(
wire: &[u8],
schema_path: &Path,
message: &str,
annotations: bool,
) -> (Vec<u8>, Vec<u8>) {
let bin = env!("CARGO_BIN_EXE_prototext");
let mut decode_cmd = Command::new(bin);
decode_cmd
.arg("--descriptor-set")
.arg(schema_path)
.arg("decode")
.args(["--type", message]);
if !annotations {
decode_cmd.arg("--no-annotations");
}
let decode_out = decode_cmd
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn prototext")
.wait_with_output_and_stdin(wire);
assert!(
decode_out.status.success(),
"prototext decode failed:\n{}",
String::from_utf8_lossy(&decode_out.stderr)
);
let text = decode_out.stdout;
let encode_out = Command::new(bin)
.arg("encode")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn prototext")
.wait_with_output_and_stdin(&text);
assert!(
encode_out.status.success(),
"prototext encode failed:\n{}",
String::from_utf8_lossy(&encode_out.stderr)
);
(text, encode_out.stdout)
}
trait SpawnExt {
fn wait_with_output_and_stdin(self, input: &[u8]) -> std::process::Output;
}
impl SpawnExt for std::process::Child {
fn wait_with_output_and_stdin(mut self, input: &[u8]) -> std::process::Output {
use std::io::Write;
if let Some(mut stdin) = self.stdin.take() {
stdin.write_all(input).ok();
}
self.wait_with_output().expect("wait_with_output failed")
}
}
#[test]
fn fixture_roundtrip_annotated_craft_a() {
let mut ran = 0;
let mut skipped = 0;
for &(name, func) in craft_a::ALL_FIXTURES {
let Some((schema_rel, message)) = index_schema(name) else {
eprintln!("SKIP {name} (not in index.toml)");
skipped += 1;
continue;
};
let wire = func();
let sp = schema_path(&schema_rel);
let (text, wire2) = cli_roundtrip(&wire, &sp, &message, true);
assert_eq!(
wire2,
wire,
"{name}: binary→text→binary round-trip must be bit-exact\n text:\n{}",
String::from_utf8_lossy(&text),
);
ran += 1;
}
eprintln!("fixture_roundtrip_annotated_craft_a: {ran} passed, {skipped} skipped");
assert!(ran > 0, "no fixtures ran");
}
#[test]
fn unknown_len_decoded_as_nested_message() {
#[rustfmt::skip]
let wire: &[u8] = &[
0xc8, 0x01, 0x2a, 0xca, 0xb2, 0x04, 0x09, 0x08, 0x07, 0x12, 0x05, b'h', b'e', b'l', b'l', b'o', ];
let sp = schema_path("fixtures/schemas/knife.pb");
let (text, wire2) = cli_roundtrip(wire, &sp, "SwissArmyKnife", true);
let text_str = String::from_utf8_lossy(&text);
assert!(
text_str.contains("9001 {"),
"unknown LEN field must be rendered as nested message, got:\n{text_str}"
);
assert_eq!(
wire2, wire,
"binary→text→binary round-trip must be bit-exact\n text:\n{text_str}"
);
}
#[test]
fn fixture_no_panic_no_annotations() {
let mut ran = 0;
let mut skipped = 0;
let bin = env!("CARGO_BIN_EXE_prototext");
for &(name, func) in craft_a::ALL_FIXTURES {
let Some((schema_rel, message)) = index_schema(name) else {
eprintln!("SKIP {name} (not in index.toml)");
skipped += 1;
continue;
};
let wire = func();
let sp = schema_path(&schema_rel);
let out = Command::new(bin)
.arg("--descriptor-set")
.arg(&sp)
.arg("decode")
.args(["--type", &message])
.arg("--no-annotations")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn prototext")
.wait_with_output_and_stdin(&wire);
assert!(
out.status.success(),
"{name}: prototext decode --no-annotations must exit 0:\n{}",
String::from_utf8_lossy(&out.stderr)
);
ran += 1;
}
eprintln!("fixture_no_panic_no_annotations: {ran} passed, {skipped} skipped");
assert!(ran > 0, "no fixtures ran");
}