use std::path::{Path, PathBuf};
use prototext_core::{parse_schema, render_as_bytes, render_as_text, RenderOpts};
#[derive(Debug, Clone)]
struct Fixture {
name: String,
schema: String,
message: String,
}
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
}
fn load_fixtures() -> Vec<Fixture> {
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()
.map(|entry| Fixture {
name: entry["name"].as_str().unwrap().to_owned(),
schema: entry["schema"].as_str().unwrap().to_owned(),
message: entry["message"].as_str().unwrap().to_owned(),
})
.collect()
}
fn load_case_text(name: &str) -> Option<Vec<u8>> {
let path = repo_root()
.join("fixtures/cases")
.join(format!("{name}.pb"));
match std::fs::read(&path) {
Ok(b) => Some(b),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => panic!("cannot read {}: {e}", path.display()),
}
}
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 load_schema(schema_rel: &str, message: &str) -> prototext_core::ParsedSchema {
let path = schema_path(schema_rel);
let bytes = std::fs::read(&path)
.unwrap_or_else(|e| panic!("cannot read schema {}: {e}", path.display()));
parse_schema(&bytes, message)
.unwrap_or_else(|e| panic!("cannot parse schema {schema_rel}:{message}: {e}"))
}
fn enum_schema() -> prototext_core::ParsedSchema {
load_schema("fixtures/schemas/enum_collision.pb", "EnumCollision")
}
fn opts(annotations: bool) -> RenderOpts {
RenderOpts::new(true, annotations, 1)
}
#[test]
fn enum_known_value_renders_symbolic_name() {
let wire = vec![0x10, 0x01];
let schema = enum_schema();
let text = render_as_text(&wire, Some(&schema), opts(true)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("GREEN"),
"expected symbolic name GREEN in: {text_str}"
);
assert!(
text_str.contains("Color(1)"),
"expected Color(1) in annotation: {text_str}"
);
}
#[test]
fn enum_unknown_value_renders_numeric_with_enum_unknown() {
let wire = vec![0x10, 0x63]; let schema = enum_schema();
let text = render_as_text(&wire, Some(&schema), opts(true)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("99"),
"expected numeric 99 in: {text_str}"
);
assert!(
text_str.contains("ENUM_UNKNOWN"),
"expected ENUM_UNKNOWN in annotation: {text_str}"
);
}
#[test]
fn packed_enum_renders_symbolic_names() {
let wire = vec![0x2A, 0x02, 0x01, 0x02]; let schema = enum_schema();
let text = render_as_text(&wire, Some(&schema), opts(true)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("GREEN"),
"expected GREEN in packed: {text_str}"
);
assert!(
text_str.contains("BLUE"),
"expected BLUE in packed: {text_str}"
);
assert!(
text_str.contains("Color([1, 2])"),
"expected Color([1, 2]) in annotation: {text_str}"
);
}
#[test]
fn enum_annotation_roundtrips_wire() {
let wire = vec![0x10, 0x01];
let schema = enum_schema();
let text = render_as_text(&wire, Some(&schema), opts(true)).unwrap();
let wire2 = render_as_bytes(&text, opts(true)).unwrap();
assert_eq!(wire2, wire, "enum annotation must round-trip byte-for-byte");
}
#[test]
fn no_annotations_omits_unknown_fields() {
let wire = vec![0xb8, 0x06, 0x01]; let schema = enum_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
!text_str.contains("99"),
"unknown field should be omitted without annotations: {text_str}"
);
}
#[test]
fn enum_named_float_roundtrip_is_varint() {
let wire = vec![0x08, 0x01];
let schema = enum_schema();
let text = render_as_text(&wire, Some(&schema), opts(true)).unwrap();
let wire2 = render_as_bytes(&text, opts(true)).unwrap();
assert_eq!(
wire2, wire,
"enum named 'float' must round-trip as varint (2 bytes), not fixed32 (5 bytes)"
);
assert_eq!(
wire2.len(),
2,
"re-encoded wire must be 2 bytes (varint), not 5 (fixed32)"
);
}
fn to_wire(text: &[u8]) -> Vec<u8> {
let opts = RenderOpts::new(true, true, 1);
render_as_bytes(text, opts).expect("render_as_bytes failed")
}
#[test]
fn fixture_roundtrip_annotated() {
let fixtures = load_fixtures();
let mut ran = 0;
let mut skipped = 0;
for fx in &fixtures {
let Some(text) = load_case_text(&fx.name) else {
eprintln!("SKIP {} (case file missing)", fx.name);
skipped += 1;
continue;
};
let wire = to_wire(&text);
let schema = load_schema(&fx.schema, &fx.message);
let opts = RenderOpts::new(true, true, 1);
let text2 = render_as_text(&wire, Some(&schema), opts).expect("render_as_text failed");
assert_eq!(
text2,
text,
"round-trip mismatch for {} (annotations=true)\n orig:\n{}\n reenc:\n{}",
fx.name,
String::from_utf8_lossy(&text),
String::from_utf8_lossy(&text2),
);
ran += 1;
}
eprintln!("roundtrip(annotations=true): {ran} passed, {skipped} skipped");
assert!(
ran > 0,
"no fixtures ran — index.toml empty or all case files missing"
);
}