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 knife_schema() -> prototext_core::ParsedSchema {
load_schema("fixtures/schemas/knife.pb", "SwissArmyKnife")
}
fn float_wire(bits: u32) -> Vec<u8> {
let mut v = vec![0xB5, 0x01]; v.extend_from_slice(&bits.to_le_bytes());
v
}
fn double_wire(bits: u64) -> Vec<u8> {
let mut v = vec![0xA9, 0x01]; v.extend_from_slice(&bits.to_le_bytes());
v
}
fn float_packed_wire(bits: &[u32]) -> Vec<u8> {
let payload: Vec<u8> = bits.iter().flat_map(|b| b.to_le_bytes()).collect();
let mut v = vec![0x92, 0x05]; v.push(payload.len() as u8);
v.extend_from_slice(&payload);
v
}
fn double_packed_wire(bits: &[u64]) -> Vec<u8> {
let payload: Vec<u8> = bits.iter().flat_map(|b| b.to_le_bytes()).collect();
let mut v = vec![0x8A, 0x05]; v.push(payload.len() as u8);
v.extend_from_slice(&payload);
v
}
const F32_CANONICAL_NAN: u32 = 0x7FC00000;
const F32_SIGNALING_NAN: u32 = 0x7F800001; const F32_SIGNED_NAN: u32 = 0xFFC00000; const F32_PAYLOAD_NAN: u32 = 0x7FC0CAFE;
const F64_CANONICAL_NAN: u64 = 0x7FF8000000000000;
const F64_SIGNALING_NAN: u64 = 0x7FF0000000000001; const F64_SIGNED_NAN: u64 = 0xFFF8000000000000; const F64_PAYLOAD_NAN: u64 = 0x7FF800000BADC0DE;
#[test]
fn float_canonical_nan_renders_bare_nan() {
let wire = float_wire(F32_CANONICAL_NAN);
let schema = knife_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("nan"),
"expected bare 'nan' for canonical NaN, got: {text_str}"
);
assert!(
!text_str.contains("nan("),
"canonical NaN must not have a modifier, got: {text_str}"
);
}
#[test]
fn float_noncanonical_nan_renders_with_modifier() {
for &bits in &[F32_SIGNALING_NAN, F32_SIGNED_NAN, F32_PAYLOAD_NAN] {
let wire = float_wire(bits);
let schema = knife_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
let expected = format!("nan(0x{:08x})", bits);
assert!(
text_str.contains(&expected),
"bits 0x{:08x}: expected '{}' in: {text_str}",
bits,
expected
);
}
}
#[test]
fn float_noncanonical_nan_roundtrips() {
for &bits in &[F32_SIGNALING_NAN, F32_SIGNED_NAN, F32_PAYLOAD_NAN] {
let wire = float_wire(bits);
let schema = knife_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,
"float NaN 0x{:08x} must round-trip bit-exact",
bits
);
}
}
#[test]
fn double_canonical_nan_renders_bare_nan() {
let wire = double_wire(F64_CANONICAL_NAN);
let schema = knife_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("nan"),
"expected bare 'nan' for canonical NaN, got: {text_str}"
);
assert!(
!text_str.contains("nan("),
"canonical NaN must not have a modifier, got: {text_str}"
);
}
#[test]
fn double_noncanonical_nan_renders_with_modifier() {
for &bits in &[F64_SIGNALING_NAN, F64_SIGNED_NAN, F64_PAYLOAD_NAN] {
let wire = double_wire(bits);
let schema = knife_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
let expected = format!("nan(0x{:016x})", bits);
assert!(
text_str.contains(&expected),
"bits 0x{:016x}: expected '{}' in: {text_str}",
bits,
expected
);
}
}
#[test]
fn double_noncanonical_nan_roundtrips() {
for &bits in &[F64_SIGNALING_NAN, F64_SIGNED_NAN, F64_PAYLOAD_NAN] {
let wire = double_wire(bits);
let schema = knife_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,
"double NaN 0x{:016x} must round-trip bit-exact",
bits
);
}
}
#[test]
fn float_packed_noncanonical_nan_renders_with_modifier() {
let wire = float_packed_wire(&[F32_CANONICAL_NAN, F32_SIGNALING_NAN, F32_PAYLOAD_NAN]);
let schema = knife_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("nan,") || text_str.contains("nan]"),
"expected bare nan element in packed, got: {text_str}"
);
assert!(
text_str.contains(&format!("nan(0x{:08x})", F32_SIGNALING_NAN)),
"expected signaling NaN modifier in packed, got: {text_str}"
);
assert!(
text_str.contains(&format!("nan(0x{:08x})", F32_PAYLOAD_NAN)),
"expected payload NaN modifier in packed, got: {text_str}"
);
}
#[test]
fn float_packed_noncanonical_nan_roundtrips() {
let wire = float_packed_wire(&[F32_CANONICAL_NAN, F32_SIGNALING_NAN, F32_PAYLOAD_NAN]);
let schema = knife_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,
"packed float NaN array must round-trip bit-exact"
);
}
#[test]
fn double_packed_noncanonical_nan_renders_with_modifier() {
let wire = double_packed_wire(&[F64_CANONICAL_NAN, F64_SIGNALING_NAN, F64_PAYLOAD_NAN]);
let schema = knife_schema();
let text = render_as_text(&wire, Some(&schema), opts(false)).unwrap();
let text_str = String::from_utf8(text).unwrap();
assert!(
text_str.contains("nan,") || text_str.contains("nan]"),
"expected bare nan element in packed, got: {text_str}"
);
assert!(
text_str.contains(&format!("nan(0x{:016x})", F64_SIGNALING_NAN)),
"expected signaling NaN modifier in packed, got: {text_str}"
);
assert!(
text_str.contains(&format!("nan(0x{:016x})", F64_PAYLOAD_NAN)),
"expected payload NaN modifier in packed, got: {text_str}"
);
}
#[test]
fn double_packed_noncanonical_nan_roundtrips() {
let wire = double_packed_wire(&[F64_CANONICAL_NAN, F64_SIGNALING_NAN, F64_PAYLOAD_NAN]);
let schema = knife_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,
"packed double NaN array must round-trip bit-exact"
);
}
#[test]
fn float_packed_all_nan_variants_roundtrip() {
let wire = float_packed_wire(&[
F32_CANONICAL_NAN,
F32_SIGNALING_NAN,
F32_SIGNED_NAN,
F32_PAYLOAD_NAN,
]);
let schema = knife_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,
"packed float all-NaN-variants must round-trip bit-exact"
);
}
#[test]
fn double_packed_all_nan_variants_roundtrip() {
let wire = double_packed_wire(&[
F64_CANONICAL_NAN,
F64_SIGNALING_NAN,
F64_SIGNED_NAN,
F64_PAYLOAD_NAN,
]);
let schema = knife_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,
"packed double all-NaN-variants must round-trip bit-exact"
);
}
#[test]
fn float_subnormals_roundtrip() {
let cases: &[u32] = &[
0x00000001, 0x007fffff, 0x80000001, 0x003fffff, ];
for &bits in cases {
let wire = float_wire(bits);
let schema = knife_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,
"float subnormal 0x{:08x} must round-trip bit-exact",
bits
);
}
}
#[test]
fn double_subnormals_roundtrip() {
let cases: &[u64] = &[
0x0000000000000001, 0x000fffffffffffff, 0x8000000000000001, 0x0004000000000000, ];
for &bits in cases {
let wire = double_wire(bits);
let schema = knife_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,
"double subnormal 0x{:016x} must round-trip bit-exact",
bits
);
}
}
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"
);
}