use super::*;
#[test]
fn test_wkt_auto_mapping_not_suppressed_by_sub_package() {
let config = CodeGenConfig {
extern_paths: vec![(
".google.protobuf.compiler".into(),
"::compiler_protos".into(),
)],
..Default::default()
};
let effective = effective_extern_paths(&[], &[], &config);
assert!(effective
.iter()
.any(|(p, _)| p == ".google.protobuf.compiler"));
assert!(
effective.iter().any(|(p, _)| p == ".google.protobuf"),
"WKT auto-mapping must coexist with sub-package extern_path"
);
}
#[test]
fn test_wkt_auto_mapping_suppressed_by_exact_match() {
let config = CodeGenConfig {
extern_paths: vec![(".google.protobuf".into(), "::my_wkts".into())],
..Default::default()
};
let effective = effective_extern_paths(&[], &[], &config);
let count = effective
.iter()
.filter(|(p, _)| p == ".google.protobuf")
.count();
assert_eq!(count, 1);
assert!(effective
.iter()
.any(|(p, r)| p == ".google.protobuf" && r == "::my_wkts"));
}
#[test]
fn test_file_extern_paths_default_injection() {
let config = CodeGenConfig::default();
let file_paths = effective_file_extern_paths(&[], &config);
assert_eq!(
file_paths,
vec![
(
"google/protobuf/descriptor.proto".to_string(),
"::buffa_descriptor::generated::descriptor".to_string(),
),
(
"google/protobuf/compiler/plugin.proto".to_string(),
"::buffa_descriptor::generated::compiler".to_string(),
),
],
);
}
#[test]
fn test_file_extern_paths_suppressed_by_user_wkt_override() {
let config = CodeGenConfig {
extern_paths: vec![(".google.protobuf".into(), "::my_wkts".into())],
..Default::default()
};
let file_paths = effective_file_extern_paths(&[], &config);
assert!(
file_paths.is_empty(),
"user .google.protobuf override must suppress descriptor file-level mapping"
);
}
#[test]
fn test_file_extern_paths_sub_package_override_suppresses_only_covered_file() {
let config = CodeGenConfig {
extern_paths: vec![(
".google.protobuf.compiler".into(),
"::compiler_protos".into(),
)],
..Default::default()
};
let file_paths = effective_file_extern_paths(&[], &config);
assert!(
file_paths
.iter()
.any(|(f, _)| f == "google/protobuf/descriptor.proto"),
"sub-package override for compiler must not suppress descriptor.proto file-level mapping"
);
assert!(
!file_paths
.iter()
.any(|(f, _)| f == "google/protobuf/compiler/plugin.proto"),
"sub-package override for compiler must suppress plugin.proto file-level mapping"
);
}
#[test]
fn test_file_extern_paths_suppressed_when_generating_descriptor_proto() {
let config = CodeGenConfig::default();
let file_paths =
effective_file_extern_paths(&["google/protobuf/descriptor.proto".to_string()], &config);
assert!(
!file_paths
.iter()
.any(|(f, _)| f == "google/protobuf/descriptor.proto"),
"must not externalize descriptor.proto when generating it locally"
);
assert!(
file_paths
.iter()
.any(|(f, _)| f == "google/protobuf/compiler/plugin.proto"),
"plugin.proto suppression is independent of descriptor.proto"
);
}
#[test]
fn test_descriptor_enum_field_resolves_to_buffa_descriptor() {
let mut descriptor_file = proto3_file("google/protobuf/descriptor.proto");
descriptor_file.package = Some("google.protobuf".to_string());
descriptor_file.message_type.push(DescriptorProto {
name: Some("FieldDescriptorProto".to_string()),
enum_type: vec![EnumDescriptorProto {
name: Some("Type".to_string()),
value: vec![enum_value("TYPE_DOUBLE", 1)],
..Default::default()
}],
..Default::default()
});
let mut user_file = proto3_file("my/uses_descriptor.proto");
user_file.package = Some("my".to_string());
user_file.dependency = vec!["google/protobuf/descriptor.proto".to_string()];
user_file.message_type.push(DescriptorProto {
name: Some("Wraps".to_string()),
field: vec![FieldDescriptorProto {
name: Some("field_type".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".google.protobuf.FieldDescriptorProto.Type".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[descriptor_file, user_file],
&["my/uses_descriptor.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = joined(&files);
assert!(
content.contains("::buffa_descriptor::generated::descriptor::field_descriptor_proto::Type"),
"descriptor enum field must resolve to buffa-descriptor: {content}"
);
assert!(
!content.contains("::buffa_types::google::protobuf::field_descriptor_proto::Type"),
"descriptor enum field must not resolve to buffa-types (does not exist): {content}"
);
}
#[test]
fn test_user_wkt_override_still_covers_descriptor_types() {
let mut descriptor_file = proto3_file("google/protobuf/descriptor.proto");
descriptor_file.package = Some("google.protobuf".to_string());
descriptor_file.message_type.push(DescriptorProto {
name: Some("FieldDescriptorProto".to_string()),
enum_type: vec![EnumDescriptorProto {
name: Some("Type".to_string()),
value: vec![enum_value("TYPE_DOUBLE", 1)],
..Default::default()
}],
..Default::default()
});
let mut user_file = proto3_file("my/uses_descriptor.proto");
user_file.package = Some("my".to_string());
user_file.dependency = vec!["google/protobuf/descriptor.proto".to_string()];
user_file.message_type.push(DescriptorProto {
name: Some("Wraps".to_string()),
field: vec![FieldDescriptorProto {
name: Some("field_type".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".google.protobuf.FieldDescriptorProto.Type".to_string()),
..Default::default()
}],
..Default::default()
});
let config = CodeGenConfig {
extern_paths: vec![(".google.protobuf".into(), "::my_wkts".into())],
..Default::default()
};
let files = generate(
&[descriptor_file, user_file],
&["my/uses_descriptor.proto".to_string()],
&config,
)
.expect("should generate");
let content = joined(&files);
assert!(
content.contains("::my_wkts::field_descriptor_proto::Type"),
"user .google.protobuf override must cover descriptor types: {content}"
);
assert!(
!content.contains("::buffa_descriptor"),
"auto-injected descriptor mapping must yield to user override: {content}"
);
}
fn plugin_proto_fixture() -> (FileDescriptorProto, FileDescriptorProto) {
let mut plugin_file = proto3_file("google/protobuf/compiler/plugin.proto");
plugin_file.package = Some("google.protobuf.compiler".to_string());
plugin_file.message_type.push(DescriptorProto {
name: Some("CodeGeneratorRequest".to_string()),
..Default::default()
});
let mut user_file = proto3_file("my/uses_plugin.proto");
user_file.package = Some("my".to_string());
user_file.dependency = vec!["google/protobuf/compiler/plugin.proto".to_string()];
user_file.message_type.push(DescriptorProto {
name: Some("Wraps".to_string()),
field: vec![FieldDescriptorProto {
name: Some("req".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_MESSAGE),
type_name: Some(".google.protobuf.compiler.CodeGeneratorRequest".to_string()),
..Default::default()
}],
..Default::default()
});
(plugin_file, user_file)
}
#[test]
fn test_plugin_proto_message_field_resolves_to_buffa_descriptor() {
let (plugin_file, user_file) = plugin_proto_fixture();
let files = generate(
&[plugin_file, user_file],
&["my/uses_plugin.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = joined(&files);
assert!(
content.contains("::buffa_descriptor::generated::compiler::CodeGeneratorRequest"),
"plugin.proto types must resolve to buffa-descriptor: {content}"
);
assert!(
!content.contains("::buffa_types::google::protobuf::compiler"),
"plugin.proto types must not resolve to buffa-types (does not exist): {content}"
);
}
#[test]
fn test_user_compiler_sub_package_override_still_covers_plugin_types() {
let (plugin_file, user_file) = plugin_proto_fixture();
let config = CodeGenConfig {
extern_paths: vec![(".google.protobuf.compiler".into(), "::my_compiler".into())],
..Default::default()
};
let files = generate(
&[plugin_file, user_file],
&["my/uses_plugin.proto".to_string()],
&config,
)
.expect("should generate");
let content = joined(&files);
assert!(
content.contains("::my_compiler::CodeGeneratorRequest"),
"user .google.protobuf.compiler override must cover plugin types: {content}"
);
assert!(
!content.contains("::buffa_descriptor"),
"auto-injected plugin mapping must yield to user override: {content}"
);
}
#[test]
fn test_empty_file() {
let file = proto3_file("empty.proto");
let result = generate(
&[file],
&["empty.proto".to_string()],
&CodeGenConfig::default(),
);
let files = result.expect("empty file should generate without error");
assert_eq!(files.len(), 1);
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert_eq!(stitcher.name, "__buffa.mod.rs");
assert!(
stitcher.content.contains("@generated"),
"missing header comment"
);
assert!(
!stitcher.content.contains("pub mod __buffa"),
"empty file should not emit a `pub mod __buffa` wrapper:\n{}",
stitcher.content
);
}
#[test]
fn test_empty_message_omits_empty_ancillary_files() {
let mut file = proto3_file("example/v1/empty.proto");
file.package = Some("example.v1".to_string());
file.message_type.push(DescriptorProto {
name: Some("Empty".to_string()),
..Default::default()
});
let files = generate(
&[file],
&["example/v1/empty.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("empty-message file should generate");
let names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect();
assert!(
files
.iter()
.any(|f| f.name == "example.v1.empty.rs" && f.kind == GeneratedFileKind::Owned),
"owned file with the empty message should still be emitted; got {names:?}"
);
assert!(
files
.iter()
.any(|f| f.kind == GeneratedFileKind::PackageMod),
"package mod should be generated; got {names:?}"
);
assert!(
!files.iter().any(|f| f.name.ends_with(".__oneof.rs")),
"empty oneof companion file should not be emitted; got {names:?}"
);
assert!(
!files.iter().any(|f| f.name.ends_with(".__view_oneof.rs")),
"empty view-oneof companion file should not be emitted; got {names:?}"
);
assert!(
!files.iter().any(|f| f.name.ends_with(".__ext.rs")),
"empty extension companion file should not be emitted; got {names:?}"
);
let package_mod = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("package mod should be generated");
assert!(
!package_mod.content.contains("example.v1.empty.__oneof.rs"),
"package mod should not include omitted oneof companion:\n{}",
package_mod.content
);
assert!(
!package_mod
.content
.contains("example.v1.empty.__view_oneof.rs"),
"package mod should not include omitted view-oneof companion:\n{}",
package_mod.content
);
assert!(
!package_mod.content.contains("example.v1.empty.__ext.rs"),
"package mod should not include omitted ext companion:\n{}",
package_mod.content
);
}
#[test]
fn stitcher_omits_empty_inner_modules_for_empty_message() {
let mut file = proto3_file("only_msg.proto");
file.package = Some("p".to_string());
file.message_type.push(DescriptorProto {
name: Some("Empty".to_string()),
..Default::default()
});
let files = generate(
&[file],
&["only_msg.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert!(
stitcher.content.contains("pub mod __buffa"),
"expected `__buffa` wrapper (view module is non-empty):\n{}",
stitcher.content
);
assert!(
stitcher.content.contains("pub mod view"),
"expected `pub mod view`:\n{}",
stitcher.content
);
assert!(
!stitcher.content.contains("pub mod oneof"),
"should not emit empty `pub mod oneof` (covers both top-level \
and nested view::oneof):\n{}",
stitcher.content
);
assert!(
!stitcher.content.contains("pub mod ext"),
"should not emit empty `pub mod ext`:\n{}",
stitcher.content
);
}
#[test]
fn stitcher_omits_view_module_when_views_disabled_and_only_oneof_present() {
let mut file = proto3_file("only_oneof.proto");
file.package = Some("p".to_string());
file.message_type.push(DescriptorProto {
name: Some("M".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("x".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
oneof_index: Some(0),
..Default::default()
},
FieldDescriptorProto {
name: Some("y".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_STRING),
oneof_index: Some(0),
..Default::default()
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("kind".to_string()),
..Default::default()
}],
..Default::default()
});
let config = CodeGenConfig {
generate_views: false,
..Default::default()
};
let files =
generate(&[file], &["only_oneof.proto".to_string()], &config).expect("should generate");
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert!(
stitcher.content.contains("pub mod __buffa"),
"expected `__buffa` wrapper (oneof module is non-empty):\n{}",
stitcher.content
);
assert!(
stitcher.content.contains("pub mod oneof"),
"expected `pub mod oneof`:\n{}",
stitcher.content
);
assert!(
!stitcher.content.contains("pub mod view"),
"should not emit `pub mod view` when views are disabled:\n{}",
stitcher.content
);
assert!(
!stitcher.content.contains("pub mod ext"),
"should not emit empty `pub mod ext`:\n{}",
stitcher.content
);
}
#[test]
fn stitcher_emits_only_ext_module_for_extension_only_proto() {
let mut file = proto3_file("ext_only.proto");
file.package = Some("p".to_string());
file.message_type = vec![DescriptorProto {
name: Some("Target".to_string()),
extension_range: vec![
crate::generated::descriptor::descriptor_proto::ExtensionRange {
start: Some(100),
end: Some(200),
..Default::default()
},
],
..Default::default()
}];
file.extension = vec![{
let mut f = make_field("my_opt", 100, Label::LABEL_OPTIONAL, Type::TYPE_STRING);
f.extendee = Some(".p.Target".to_string());
f
}];
let files = generate(
&[file],
&["ext_only.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert!(
stitcher.content.contains("pub mod __buffa"),
"expected `__buffa` wrapper (ext module is non-empty):\n{}",
stitcher.content
);
assert!(
stitcher.content.contains("pub mod ext"),
"expected `pub mod ext`:\n{}",
stitcher.content
);
assert!(
stitcher.content.contains("pub mod view"),
"expected `pub mod view` for `TargetView`:\n{}",
stitcher.content
);
assert!(
!stitcher.content.contains("pub mod oneof"),
"should not emit empty `pub mod oneof`:\n{}",
stitcher.content
);
}
#[test]
fn test_package_to_mod_filename() {
assert_eq!(
package_to_mod_filename("google.protobuf"),
"google.protobuf.mod.rs"
);
assert_eq!(package_to_mod_filename("foo"), "foo.mod.rs");
assert_eq!(package_to_mod_filename(""), "__buffa.mod.rs");
assert_eq!(
proto_path_to_stem("google/protobuf/timestamp.proto"),
"google.protobuf.timestamp"
);
}
#[test]
fn test_multi_file_same_package_merged() {
let mut a = proto3_file("a.proto");
a.package = Some("shared.pkg".to_string());
a.message_type.push(DescriptorProto {
name: Some("A".to_string()),
..Default::default()
});
let mut b = proto3_file("b.proto");
b.package = Some("shared.pkg".to_string());
b.message_type.push(DescriptorProto {
name: Some("B".to_string()),
..Default::default()
});
let files = generate(
&[a, b],
&["a.proto".to_string(), "b.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("multi-file package should merge");
assert_eq!(files.len(), 5);
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert_eq!(stitcher.name, "shared.pkg.mod.rs");
let content = &joined(&files);
assert!(content.contains("pub struct A"));
assert!(content.contains("pub struct B"));
assert_eq!(stitcher.content.matches("pub mod __buffa {").count(), 1);
assert!(stitcher.content.contains(r#"include!("a.__view.rs");"#));
assert!(stitcher.content.contains(r#"include!("b.__view.rs");"#));
}
#[test]
fn test_package_to_filename() {
assert_eq!(package_to_filename("google.protobuf"), "google.protobuf.rs");
assert_eq!(package_to_filename("foo"), "foo.rs");
assert_eq!(package_to_filename(""), "__buffa.rs");
}
fn shared_pkg_fixture() -> ([FileDescriptorProto; 2], [String; 2]) {
let mut a = proto3_file("a.proto");
a.package = Some("shared.pkg".to_string());
a.message_type.push(DescriptorProto {
name: Some("A".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("x".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
oneof_index: Some(0),
..Default::default()
},
FieldDescriptorProto {
name: Some("y".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_STRING),
oneof_index: Some(0),
..Default::default()
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("kind".to_string()),
..Default::default()
}],
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let mut b = proto3_file("b.proto");
b.package = Some("shared.pkg".to_string());
b.message_type.push(DescriptorProto {
name: Some("B".to_string()),
..Default::default()
});
([a, b], ["a.proto".to_string(), "b.proto".to_string()])
}
#[test]
fn test_file_per_package_multi_file() {
let (descs, names) = shared_pkg_fixture();
let config = CodeGenConfig {
file_per_package: true,
..Default::default()
};
let files = generate(&descs, &names, &config).expect("file_per_package should generate");
assert_eq!(files.len(), 1, "expected single per-package file");
let pkg = &files[0];
assert_eq!(pkg.name, "shared.pkg.rs");
assert_eq!(pkg.kind, GeneratedFileKind::PackageMod);
assert!(pkg.content.contains("pub struct A"));
assert!(pkg.content.contains("pub struct B"));
assert!(
!pkg.content.contains("include!"),
"per-package file must inline content, not include! per-file outputs"
);
assert_eq!(pkg.content.matches("pub mod __buffa {").count(), 1);
assert!(pkg.content.contains("pub mod view {"));
assert!(pkg.content.contains("pub mod oneof {"));
assert!(
!pkg.content.contains("pub mod ext {"),
"no extensions in fixture → empty `pub mod ext` should be omitted"
);
}
#[test]
fn test_file_per_package_module_structure_matches_stitcher() {
let (descs, names) = shared_pkg_fixture();
let per_file = generate(&descs, &names, &CodeGenConfig::default()).unwrap();
let stitcher = per_file
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.unwrap();
let per_package = generate(
&descs,
&names,
&CodeGenConfig {
file_per_package: true,
..Default::default()
},
)
.unwrap();
let pkg = &per_package[0];
fn strip_header(s: &str) -> &str {
s.find("\n\n").map_or(s, |i| &s[i + 2..])
}
let mut spliced = stitcher.content.clone();
for f in per_file
.iter()
.filter(|f| f.kind != GeneratedFileKind::PackageMod)
{
let needle = format!(r#"include!("{}");"#, f.name);
spliced = spliced.replace(&needle, strip_header(&f.content));
}
assert!(
!spliced.contains("include!"),
"splice missed an include: {spliced}"
);
let mod_decls = |s: &str| -> Vec<(usize, String)> {
let mut depth = 0usize;
let mut out = Vec::new();
for l in s.lines() {
let trimmed = l.trim_start();
if let Some(rest) = trimmed.strip_prefix("pub mod ") {
out.push((depth, rest.trim_end_matches(" {").to_string()));
}
depth += l.matches('{').count();
depth = depth.saturating_sub(l.matches('}').count());
}
out
};
let spliced_mods = mod_decls(&spliced);
let pkg_mods = mod_decls(&pkg.content);
assert!(
spliced_mods.len() > 5,
"fixture should produce >5 pub mod decls, got {}: {spliced_mods:?}",
spliced_mods.len()
);
assert_eq!(spliced_mods, pkg_mods);
}
#[test]
fn test_file_per_package_register_types_with_text() {
let (descs, names) = shared_pkg_fixture();
let config = CodeGenConfig {
file_per_package: true,
generate_text: true,
..Default::default()
};
let files = generate(&descs, &names, &config).unwrap();
assert_eq!(files.len(), 1);
let content = &files[0].content;
assert!(
content.contains("pub fn register_types("),
"register_types fn missing: {content}"
);
assert!(
content.contains("reg.register_text_any(super::__A_TEXT_ANY)"),
"A text-any path: {content}"
);
assert!(
content.contains("reg.register_text_any(super::a::__INNER_TEXT_ANY)"),
"nested Inner text-any path: {content}"
);
assert!(
content.contains("reg.register_text_any(super::__B_TEXT_ANY)"),
"B text-any path: {content}"
);
}
#[test]
fn test_file_per_package_unnamed_package() {
let file = proto3_file("noname.proto");
let config = CodeGenConfig {
file_per_package: true,
..Default::default()
};
let files = generate(&[file], &["noname.proto".to_string()], &config)
.expect("unnamed package should generate");
assert_eq!(files.len(), 1);
assert_eq!(files[0].name, "__buffa.rs");
assert!(
!files[0].content.contains("pub mod __buffa"),
"empty package should not emit `__buffa` wrapper:\n{}",
files[0].content
);
}
#[test]
fn test_file_per_package_multiple_packages() {
let mut a = proto3_file("a.proto");
a.package = Some("alpha".to_string());
let mut b = proto3_file("b.proto");
b.package = Some("beta".to_string());
let config = CodeGenConfig {
file_per_package: true,
..Default::default()
};
let files = generate(
&[a, b],
&["a.proto".to_string(), "b.proto".to_string()],
&config,
)
.expect("multi-package should generate");
assert_eq!(files.len(), 2);
let names: Vec<_> = files.iter().map(|f| f.name.as_str()).collect();
assert_eq!(names, &["alpha.rs", "beta.rs"]);
}
#[test]
fn test_child_package_named_view_no_collision() {
let mut a = proto3_file("a.proto");
a.package = Some("foo".to_string());
a.message_type.push(DescriptorProto {
name: Some("A".to_string()),
..Default::default()
});
let mut b = proto3_file("b.proto");
b.package = Some("foo.view".to_string());
b.message_type.push(DescriptorProto {
name: Some("B".to_string()),
..Default::default()
});
let files = generate(
&[a, b],
&["a.proto".to_string(), "b.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("`package foo.view` alongside `package foo` must compile");
let stitchers: Vec<_> = files
.iter()
.filter(|f| f.kind == GeneratedFileKind::PackageMod)
.collect();
assert_eq!(stitchers.len(), 2);
for s in &stitchers {
let top_level_view = s
.content
.lines()
.any(|l| l.starts_with("pub mod view {") || l == "pub mod view {");
assert!(
!top_level_view,
"stitcher {} must not emit top-level `pub mod view`: {}",
s.name, s.content
);
assert!(s.content.contains("pub mod __buffa {"));
}
let entries: Vec<_> = stitchers
.iter()
.map(|s| {
(
s.name.clone(),
s.name.trim_end_matches(".mod.rs").to_string(),
)
})
.collect();
let tree = crate::generate_module_tree(&entries, crate::IncludeMode::Relative(""), false);
assert!(
tree.contains("pub mod foo {") && tree.contains("pub mod view {"),
"tree must nest `view` under `foo`: {tree}"
);
}
#[test]
fn test_simple_enum() {
let mut file = proto3_file("status.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Status".to_string()),
value: vec![
enum_value("UNKNOWN", 0),
enum_value("ACTIVE", 1),
enum_value("INACTIVE", 2),
],
..Default::default()
});
let files = generate(
&[file],
&["status.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("simple enum should generate");
let content = &joined(&files);
assert!(
content.contains("pub enum Status"),
"missing enum: {content}"
);
assert!(
content.contains("UNKNOWN = 0"),
"missing UNKNOWN: {content}"
);
assert!(content.contains("ACTIVE = 1"), "missing ACTIVE: {content}");
assert!(
content.contains("INACTIVE = 2"),
"missing INACTIVE: {content}"
);
assert!(
content.contains("impl ::buffa::Enumeration for Status"),
"missing Enumeration impl: {content}"
);
assert!(
content.contains("impl ::core::default::Default for Status"),
"missing Default impl: {content}"
);
}
#[test]
fn test_enum_with_alias() {
let mut file = proto3_file("code.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Code".to_string()),
value: vec![
enum_value("OK", 0),
enum_value("SUCCESS", 0), enum_value("ERROR", 1),
],
options: (crate::generated::descriptor::EnumOptions {
allow_alias: Some(true),
..Default::default()
})
.into(),
..Default::default()
});
let files = generate(
&[file],
&["code.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("aliased enum should generate");
let content = &joined(&files);
assert!(content.contains("OK = 0"), "missing primary: {content}");
assert!(
content.contains("pub const SUCCESS"),
"alias not emitted as const: {content}"
);
assert!(
!content.contains("SUCCESS = 0"),
"alias must not be a variant: {content}"
);
}
#[test]
fn test_enum_values_emits_static_slice_in_declaration_order() {
let mut file = proto3_file("status.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Status".to_string()),
value: vec![
enum_value("UNKNOWN", 0),
enum_value("ACTIVE", 1),
enum_value("INACTIVE", 2),
],
..Default::default()
});
let files = generate(
&[file],
&["status.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("enum should generate");
let content = &joined(&files);
assert!(
content.contains("&[Self::UNKNOWN, Self::ACTIVE, Self::INACTIVE]"),
"missing declaration-order values() slice: {content}"
);
}
#[test]
fn test_enum_values_skips_aliases() {
let mut file = proto3_file("code.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Code".to_string()),
value: vec![
enum_value("OK", 0),
enum_value("SUCCESS", 0), enum_value("ERROR", 1),
],
options: (crate::generated::descriptor::EnumOptions {
allow_alias: Some(true),
..Default::default()
})
.into(),
..Default::default()
});
let files = generate(
&[file],
&["code.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("aliased enum should generate");
let content = &joined(&files);
assert!(
content.contains("&[Self::OK, Self::ERROR]"),
"values() should mirror primary variants only: {content}"
);
assert!(
!content.contains("Self::SUCCESS"),
"alias `SUCCESS` must not appear in values(): {content}"
);
}
#[test]
fn test_file_not_found_error() {
let file = proto3_file("other.proto");
let result = generate(
&[file],
&["missing.proto".to_string()],
&CodeGenConfig::default(),
);
assert!(
matches!(result, Err(CodeGenError::FileNotFound(_))),
"expected FileNotFound error"
);
}
#[test]
fn test_type_url_top_level_with_package() {
let mut file = proto3_file("person.proto");
file.package = Some("my.company".to_string());
file.message_type.push(DescriptorProto {
name: Some("Person".to_string()),
..Default::default()
});
let files = generate(
&[file],
&["person.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/my.company.Person""#),
"wrong or missing TYPE_URL: {content}"
);
}
#[test]
fn test_type_url_top_level_no_package() {
let mut file = proto3_file("root.proto");
file.message_type.push(DescriptorProto {
name: Some("Root".to_string()),
..Default::default()
});
let files = generate(
&[file],
&["root.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/Root""#),
"wrong or missing TYPE_URL for no-package message: {content}"
);
}
#[test]
fn test_type_url_nested_message() {
let mut file = proto3_file("nested_type_url.proto");
file.package = Some("acme".to_string());
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["nested_type_url.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/acme.Outer""#),
"wrong Outer TYPE_URL: {content}"
);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/acme.Outer.Inner""#),
"wrong Inner TYPE_URL: {content}"
);
}
#[test]
fn test_type_url_nested_no_package() {
let mut file = proto3_file("nested_nopackage.proto");
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["nested_nopackage.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/Outer""#),
"wrong Outer TYPE_URL: {content}"
);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/Outer.Inner""#),
"wrong Inner TYPE_URL (no package): {content}"
);
}
#[test]
fn test_type_url_doubly_nested() {
let mut file = proto3_file("doubly_nested.proto");
file.package = Some("pkg".to_string());
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Middle".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["doubly_nested.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/pkg.Outer""#),
"wrong Outer TYPE_URL: {content}"
);
assert!(
content.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/pkg.Outer.Middle""#),
"wrong Middle TYPE_URL: {content}"
);
assert!(
content
.contains(r#"TYPE_URL: &'static str = "type.googleapis.com/pkg.Outer.Middle.Inner""#),
"wrong Inner TYPE_URL: {content}"
);
}
#[test]
fn test_message_name_consts() {
let mut file = proto3_file("named.proto");
file.package = Some("my.pkg".to_string());
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["named.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = joined(&files);
for snippet in [
r#"const PACKAGE: &'static str = "my.pkg""#,
r#"const NAME: &'static str = "Outer""#,
r#"const FULL_NAME: &'static str = "my.pkg.Outer""#,
r#"const TYPE_URL: &'static str = "type.googleapis.com/my.pkg.Outer""#,
r#"const NAME: &'static str = "Outer.Inner""#,
r#"const FULL_NAME: &'static str = "my.pkg.Outer.Inner""#,
] {
assert!(content.contains(snippet), "missing `{snippet}`: {content}");
}
let mut root = proto3_file("root.proto");
root.message_type.push(DescriptorProto {
name: Some("Root".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Leaf".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[root],
&["root.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = joined(&files);
for snippet in [
r#"const PACKAGE: &'static str = """#,
r#"const NAME: &'static str = "Root""#,
r#"const FULL_NAME: &'static str = "Root""#,
r#"const NAME: &'static str = "Root.Leaf""#,
r#"const FULL_NAME: &'static str = "Root.Leaf""#,
r#"const TYPE_URL: &'static str = "type.googleapis.com/Root.Leaf""#,
] {
assert!(content.contains(snippet), "missing `{snippet}`: {content}");
}
}
#[test]
fn test_message_scalar_fields() {
let mut file = proto3_file("scalars.proto");
file.message_type.push(DescriptorProto {
name: Some("Scalars".to_string()),
field: vec![
make_field("count", 1, Label::LABEL_OPTIONAL, Type::TYPE_INT32),
make_field("active", 2, Label::LABEL_OPTIONAL, Type::TYPE_BOOL),
make_field("score", 3, Label::LABEL_OPTIONAL, Type::TYPE_DOUBLE),
],
..Default::default()
});
let files = generate(
&[file],
&["scalars.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("scalar fields message should generate");
let content = &joined(&files);
assert!(
content.contains("pub struct Scalars"),
"missing struct: {content}"
);
assert!(
content.contains("pub count: i32"),
"missing count field: {content}"
);
assert!(
content.contains("pub active: bool"),
"missing active field: {content}"
);
assert!(
content.contains("pub score: f64"),
"missing score field: {content}"
);
assert!(
content.contains("impl ::buffa::DefaultInstance for Scalars"),
"missing DefaultInstance impl: {content}"
);
assert!(
content.contains("impl ::buffa::Message for Scalars"),
"missing Message impl: {content}"
);
assert!(
content.contains("fn compute_size"),
"missing compute_size: {content}"
);
assert!(
content.contains("fn merge_field"),
"missing merge_field: {content}"
);
}
#[test]
fn test_message_nested_message_field() {
let mut file = proto3_file("nested.proto");
file.message_type.push(DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
});
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
field: vec![FieldDescriptorProto {
name: Some("inner".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_MESSAGE),
type_name: Some(".Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["nested.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("nested message should generate");
let content = &joined(&files);
assert!(
content.contains("pub struct Outer"),
"missing Outer: {content}"
);
assert!(
content.contains("pub inner: ::buffa::MessageField<Inner>"),
"missing MessageField: {content}"
);
assert!(
content.contains("compute_size"),
"missing compute_size call for sub-message: {content}"
);
assert!(
content.contains("merge_length_delimited"),
"missing merge_length_delimited for sub-message: {content}"
);
assert!(
content.contains("get_or_insert_default"),
"missing get_or_insert_default in merge: {content}"
);
}
#[test]
fn test_message_map_field() {
let mut file = proto3_file("withmap.proto");
let map_entry = DescriptorProto {
name: Some("AttrsEntry".to_string()),
field: vec![
make_field("key", 1, Label::LABEL_OPTIONAL, Type::TYPE_STRING),
make_field("value", 2, Label::LABEL_OPTIONAL, Type::TYPE_INT32),
],
options: (MessageOptions {
map_entry: Some(true),
..Default::default()
})
.into(),
..Default::default()
};
file.message_type.push(DescriptorProto {
name: Some("WithMap".to_string()),
field: vec![FieldDescriptorProto {
name: Some("attrs".to_string()),
number: Some(1),
label: Some(Label::LABEL_REPEATED),
r#type: Some(Type::TYPE_MESSAGE),
type_name: Some(".WithMap.AttrsEntry".to_string()),
..Default::default()
}],
nested_type: vec![map_entry],
..Default::default()
});
let files = generate(
&[file],
&["withmap.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("map field message should generate");
let content = &joined(&files);
assert!(
content.contains("pub struct WithMap"),
"missing struct: {content}"
);
assert!(
content.contains("pub attrs:"),
"missing attrs field: {content}"
);
assert!(
content.contains("::buffa::__private::HashMap"),
"map field must use ::buffa::__private::HashMap, got: {content}"
);
}
#[test]
fn test_message_oneof() {
let mut file = proto3_file("oneof.proto");
file.message_type.push(DescriptorProto {
name: Some("WithOneof".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("count".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
oneof_index: Some(0),
..Default::default()
},
FieldDescriptorProto {
name: Some("name".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_STRING),
oneof_index: Some(0),
..Default::default()
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("kind".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["oneof.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("oneof message should generate");
let content = &joined(&files);
assert!(
content.contains("pub struct WithOneof"),
"missing struct: {content}"
);
assert!(
content.contains("pub kind:"),
"missing oneof field: {content}"
);
assert!(
content.contains("pub mod with_oneof"),
"missing message module: {content}"
);
assert!(
content.contains("pub enum Kind"),
"missing oneof enum: {content}"
);
assert!(
content.contains("Count(i32)"),
"missing Count variant: {content}"
);
assert!(
content.contains("impl ::buffa::Oneof for Kind"),
"missing Oneof impl: {content}"
);
}
#[test]
fn test_message_proto3_optional() {
let mut file = proto3_file("proto3opt.proto");
file.message_type.push(DescriptorProto {
name: Some("WithOptional".to_string()),
field: vec![FieldDescriptorProto {
name: Some("count".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
oneof_index: Some(0),
proto3_optional: Some(true),
..Default::default()
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("_count".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["proto3opt.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("proto3 optional message should generate");
let content = &joined(&files);
assert!(
content.contains("pub struct WithOptional"),
"missing struct: {content}"
);
assert!(
content.contains("pub count: ::core::option::Option<i32>"),
"missing optional field: {content}"
);
assert!(
content.contains("if let Some"),
"missing if-let in impl: {content}"
);
assert!(
content.contains("Option::Some"),
"missing Some assignment in merge: {content}"
);
}
#[test]
fn test_message_proto3_optional_string() {
let mut file = proto3_file("optstr.proto");
file.message_type.push(DescriptorProto {
name: Some("WithOptStr".to_string()),
field: vec![FieldDescriptorProto {
name: Some("label".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_STRING),
oneof_index: Some(0),
proto3_optional: Some(true),
..Default::default()
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("_label".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["optstr.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("proto3 optional string should generate");
let content = &joined(&files);
assert!(
content.contains("pub label: ::core::option::Option<::buffa::alloc::string::String>"),
"missing optional string field: {content}"
);
assert!(
content.contains("encode_string"),
"missing encode_string in write_to: {content}"
);
assert!(
content.contains("merge_string"),
"missing merge_string in merge: {content}"
);
}
#[test]
fn test_message_proto3_optional_enum() {
let mut file = proto3_file("optenu.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Color".to_string()),
value: vec![enum_value("RED", 0), enum_value("BLUE", 1)],
..Default::default()
});
file.message_type.push(DescriptorProto {
name: Some("WithOptEnum".to_string()),
field: vec![FieldDescriptorProto {
name: Some("color".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".Color".to_string()),
oneof_index: Some(0),
proto3_optional: Some(true),
..Default::default()
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("_color".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["optenu.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("proto3 optional enum should generate");
let content = &joined(&files);
assert!(
content.contains("Option<::buffa::EnumValue<Color>>"),
"wrong type for optional enum: {content}"
);
assert!(
content.contains("EnumValue::from"),
"missing EnumValue::from in merge: {content}"
);
}
#[test]
fn test_message_proto3_optional_bytes_and_bool() {
let mut file = proto3_file("optmisc.proto");
file.message_type.push(DescriptorProto {
name: Some("WithOptMisc".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("data".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_BYTES),
oneof_index: Some(0),
proto3_optional: Some(true),
..Default::default()
},
FieldDescriptorProto {
name: Some("flag".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_BOOL),
oneof_index: Some(1),
proto3_optional: Some(true),
..Default::default()
},
],
oneof_decl: vec![
OneofDescriptorProto {
name: Some("_data".to_string()),
..Default::default()
},
OneofDescriptorProto {
name: Some("_flag".to_string()),
..Default::default()
},
],
..Default::default()
});
let files = generate(
&[file],
&["optmisc.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("proto3 optional bytes/bool should generate");
let content = &joined(&files);
assert!(
content.contains("Option<::buffa::alloc::vec::Vec<u8>>"),
"missing optional bytes field: {content}"
);
assert!(
content.contains("Option<bool>"),
"missing optional bool field: {content}"
);
assert!(
content.contains("is_some()"),
"fixed-size optional should use is_some(): {content}"
);
assert!(
content.contains("encode_bytes"),
"missing encode_bytes for optional bytes: {content}"
);
}
#[test]
fn test_message_string_and_bytes_fields() {
let mut file = proto3_file("strings.proto");
file.message_type.push(DescriptorProto {
name: Some("WithStrings".to_string()),
field: vec![
make_field("name", 1, Label::LABEL_OPTIONAL, Type::TYPE_STRING),
make_field("data", 2, Label::LABEL_OPTIONAL, Type::TYPE_BYTES),
],
..Default::default()
});
let files = generate(
&[file],
&["strings.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("string/bytes message should generate");
let content = &joined(&files);
assert!(
content.contains("pub name: ::buffa::alloc::string::String"),
"missing string field: {content}"
);
assert!(
content.contains("pub data: ::buffa::alloc::vec::Vec<u8>"),
"missing bytes field: {content}"
);
assert!(
content.contains("encode_string"),
"missing encode_string: {content}"
);
assert!(
content.contains("merge_string"),
"missing merge_string: {content}"
);
assert!(
content.contains("string_encoded_len"),
"missing string_encoded_len: {content}"
);
assert!(
content.contains("encode_bytes"),
"missing encode_bytes: {content}"
);
assert!(
content.contains("merge_bytes"),
"missing merge_bytes: {content}"
);
assert!(
content.contains("bytes_encoded_len"),
"missing bytes_encoded_len: {content}"
);
}
#[test]
fn test_message_enum_field() {
let mut file = proto3_file("enumfield.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Status".to_string()),
value: vec![enum_value("UNKNOWN", 0), enum_value("ACTIVE", 1)],
..Default::default()
});
file.message_type.push(DescriptorProto {
name: Some("WithEnum".to_string()),
field: vec![FieldDescriptorProto {
name: Some("status".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".Status".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["enumfield.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("enum field message should generate");
let content = &joined(&files);
assert!(
content.contains("pub status: ::buffa::EnumValue<Status>"),
"missing enum field: {content}"
);
assert!(
content.contains("to_i32()"),
"missing to_i32 in generated code: {content}"
);
assert!(
content.contains("int32_encoded_len"),
"missing int32_encoded_len in compute_size: {content}"
);
assert!(
content.contains("EnumValue::from"),
"missing EnumValue::from in generated code: {content}"
);
}
#[test]
fn test_repeated_packed_scalar() {
let mut file = proto3_file("repeatedscalar.proto");
file.message_type.push(DescriptorProto {
name: Some("WithRepeated".to_string()),
field: vec![
make_field("ids", 1, Label::LABEL_REPEATED, Type::TYPE_INT32),
make_field("scores", 2, Label::LABEL_REPEATED, Type::TYPE_DOUBLE),
],
..Default::default()
});
let files = generate(
&[file],
&["repeatedscalar.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("repeated scalar message should generate");
let content = &joined(&files);
assert!(
content.contains("pub ids: ::buffa::alloc::vec::Vec<i32>"),
"missing ids field: {content}"
);
assert!(
content.contains("pub scores: ::buffa::alloc::vec::Vec<f64>"),
"missing scores field: {content}"
);
assert!(
content.contains("is_empty()"),
"packed repeated should check is_empty: {content}"
);
assert!(
content.contains("int32_encoded_len"),
"missing int32_encoded_len in payload size: {content}"
);
assert!(
content.contains("encode_int32"),
"missing encode_int32 in write_to: {content}"
);
assert!(
content.contains("decode_int32"),
"missing decode_int32 in merge: {content}"
);
assert!(
content.contains("WireType::LengthDelimited"),
"missing packed merge branch: {content}"
);
}
#[test]
fn test_repeated_unpacked_string() {
let mut file = proto3_file("repeatedstr.proto");
file.message_type.push(DescriptorProto {
name: Some("WithRepeatedStr".to_string()),
field: vec![make_field(
"tags",
1,
Label::LABEL_REPEATED,
Type::TYPE_STRING,
)],
..Default::default()
});
let files = generate(
&[file],
&["repeatedstr.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("repeated string message should generate");
let content = &joined(&files);
assert!(
content.contains("pub tags: ::buffa::alloc::vec::Vec<::buffa::alloc::string::String>"),
"missing tags field: {content}"
);
assert!(
content.contains("string_encoded_len"),
"missing string_encoded_len: {content}"
);
assert!(
content.contains("encode_string"),
"missing encode_string: {content}"
);
assert!(
content.contains("decode_string"),
"missing decode_string: {content}"
);
}
#[test]
fn test_repeated_message_field() {
let mut file = proto3_file("repeatedmsg.proto");
file.message_type.push(DescriptorProto {
name: Some("Item".to_string()),
..Default::default()
});
file.message_type.push(DescriptorProto {
name: Some("Container".to_string()),
field: vec![FieldDescriptorProto {
name: Some("items".to_string()),
number: Some(1),
label: Some(Label::LABEL_REPEATED),
r#type: Some(Type::TYPE_MESSAGE),
type_name: Some(".Item".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["repeatedmsg.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("repeated message should generate");
let content = &joined(&files);
assert!(
content.contains("pub items: ::buffa::alloc::vec::Vec<Item>"),
"missing items field: {content}"
);
assert!(
content.contains("merge_length_delimited"),
"missing merge_length_delimited for repeated msg: {content}"
);
assert!(
content.contains("__cache.consume_next()"),
"missing SizeCache consume in write_to: {content}"
);
}
#[test]
fn test_repeated_enum_field() {
let mut file = proto3_file("repeatedenu.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Status".to_string()),
value: vec![enum_value("UNKNOWN", 0), enum_value("ACTIVE", 1)],
..Default::default()
});
file.message_type.push(DescriptorProto {
name: Some("WithRepeatedEnum".to_string()),
field: vec![FieldDescriptorProto {
name: Some("statuses".to_string()),
number: Some(1),
label: Some(Label::LABEL_REPEATED),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".Status".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["repeatedenu.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("repeated enum should generate");
let content = &joined(&files);
assert!(
content.contains("pub statuses: ::buffa::alloc::vec::Vec<::buffa::EnumValue<Status>>"),
"missing statuses field: {content}"
);
assert!(
content.contains("to_i32()"),
"missing to_i32 for packed enum write: {content}"
);
assert!(
content.contains("EnumValue::from"),
"missing EnumValue::from in packed decode: {content}"
);
}
#[test]
fn extension_set_impl_on_generated_options() {
use crate::generated::descriptor::FieldOptions;
use buffa::extension::codecs::{Int32, StringCodec};
use buffa::{Extension, ExtensionSet};
const WEIGHT: Extension<Int32> = Extension::new(50001, FieldOptions::PROTO_FQN);
const TAG: Extension<StringCodec> = Extension::new(50002, FieldOptions::PROTO_FQN);
let mut opts = FieldOptions::default();
assert!(!opts.has_extension(&WEIGHT));
opts.set_extension(&WEIGHT, -7);
opts.set_extension(&TAG, "hello".to_string());
assert_eq!(opts.extension(&WEIGHT), Some(-7));
assert_eq!(opts.extension(&TAG), Some("hello".to_string()));
assert!(opts.has_extension(&WEIGHT));
use buffa::Message;
let bytes = opts.encode_to_vec();
let decoded = FieldOptions::decode_from_slice(&bytes).expect("decode");
assert_eq!(decoded.extension(&WEIGHT), Some(-7));
assert_eq!(decoded.extension(&TAG), Some("hello".to_string()));
let file = proto3_file("ext.proto");
let files = generate(
&[FileDescriptorProto {
message_type: vec![DescriptorProto {
name: Some("M".to_string()),
..Default::default()
}],
..file
}],
&["ext.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("generate");
assert!(
files[0]
.content
.contains("impl ::buffa::ExtensionSet for M"),
"missing ExtensionSet impl: {}",
files[0].content
);
}
#[test]
fn editions_delimited_message_encoding() {
use crate::generated::descriptor::{
feature_set::MessageEncoding as FsMessageEncoding, Edition, FeatureSet, FieldOptions,
FileOptions,
};
let inner_msg = DescriptorProto {
name: Some("Inner".to_string()),
field: vec![make_field("x", 1, Label::LABEL_OPTIONAL, Type::TYPE_INT32)],
..Default::default()
};
let mut lp_field = make_field("lp_child", 2, Label::LABEL_OPTIONAL, Type::TYPE_MESSAGE);
lp_field.type_name = Some(".Inner".to_string());
lp_field.options = FieldOptions {
features: FeatureSet {
message_encoding: Some(FsMessageEncoding::LENGTH_PREFIXED),
..Default::default()
}
.into(),
..Default::default()
}
.into();
let mut delim_field = make_field("delim_child", 3, Label::LABEL_OPTIONAL, Type::TYPE_MESSAGE);
delim_field.type_name = Some(".Inner".to_string());
let mut map_field = make_field("inners", 4, Label::LABEL_REPEATED, Type::TYPE_MESSAGE);
map_field.type_name = Some(".Outer.InnersEntry".to_string());
let mut map_val = make_field("value", 2, Label::LABEL_OPTIONAL, Type::TYPE_MESSAGE);
map_val.type_name = Some(".Inner".to_string());
let map_entry = DescriptorProto {
name: Some("InnersEntry".to_string()),
field: vec![
make_field("key", 1, Label::LABEL_OPTIONAL, Type::TYPE_STRING),
map_val,
],
options: MessageOptions {
map_entry: Some(true),
..Default::default()
}
.into(),
..Default::default()
};
let outer_msg = DescriptorProto {
name: Some("Outer".to_string()),
field: vec![lp_field, delim_field, map_field],
nested_type: vec![map_entry],
..Default::default()
};
let file = FileDescriptorProto {
name: Some("delim.proto".to_string()),
edition: Some(Edition::EDITION_2023),
options: FileOptions {
features: FeatureSet {
message_encoding: Some(FsMessageEncoding::DELIMITED),
..Default::default()
}
.into(),
..Default::default()
}
.into(),
message_type: vec![inner_msg, outer_msg],
..Default::default()
};
let files = generate(
&[file],
&["delim.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("generate");
let content = &joined(&files);
assert!(
content
.contains("::buffa::encoding::Tag::new(3u32, ::buffa::encoding::WireType::StartGroup)"),
"delim_child should encode as group: {content}"
);
assert!(
content.contains("merge_group"),
"delim_child should decode via merge_group: {content}"
);
assert!(
content.contains("2u32 => {\n if tag.wire_type() != ::buffa::encoding::WireType::LengthDelimited"),
"lp_child should decode as length-delimited: {content}"
);
assert!(
!content.contains("Tag::new(2u32, ::buffa::encoding::WireType::StartGroup)"),
"lp_child should not encode as group"
);
assert_eq!(
content.matches("merge_group").count(),
1,
"owned: {content}"
);
assert_eq!(
content.matches("borrow_group").count(),
1,
"view: {content}"
);
}
#[test]
fn nested_message_cross_package_reference_uses_correct_nesting() {
let status_enum = EnumDescriptorProto {
name: Some("Status".into()),
value: vec![enum_value("UNSPECIFIED", 0), enum_value("ACTIVE", 1)],
..Default::default()
};
let biz = DescriptorProto {
name: Some("Biz".into()),
enum_type: vec![status_enum],
..Default::default()
};
let filter = DescriptorProto {
name: Some("Filter".into()),
field: vec![FieldDescriptorProto {
name: Some("status".into()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".a.v1.Biz.Status".into()),
..Default::default()
}],
..Default::default()
};
let svc = DescriptorProto {
name: Some("Svc".into()),
nested_type: vec![filter],
..Default::default()
};
let file_admin = FileDescriptorProto {
name: Some("admin.proto".into()),
package: Some("a.admin.v1".into()),
syntax: Some("proto3".into()),
message_type: vec![svc],
..Default::default()
};
let file_biz = FileDescriptorProto {
name: Some("biz.proto".into()),
package: Some("a.v1".into()),
syntax: Some("proto3".into()),
message_type: vec![biz],
..Default::default()
};
let files = generate(
&[file_admin, file_biz],
&["admin.proto".into()],
&CodeGenConfig::default(),
)
.expect("codegen should succeed");
let content = &joined(&files);
assert!(
content.contains("super::super::super::v1::biz::Status"),
"nested message should use 3 supers for cross-package reference at nesting=1.\n\
Generated code:\n{content}"
);
let path_3 = content
.matches("super::super::super::v1::biz::Status")
.count();
let path_2 = content.matches("super::super::v1::biz::Status").count();
assert_eq!(
path_3, path_2,
"every v1::biz::Status reference must use 3 supers (nesting=1), \
but found {} with 3 supers and {} total with >=2 supers.\n\
Generated code:\n{content}",
path_3, path_2
);
}
#[test]
fn doubly_nested_message_paths_use_correct_nesting() {
let status_enum = EnumDescriptorProto {
name: Some("Status".into()),
value: vec![enum_value("UNSPECIFIED", 0), enum_value("ACTIVE", 1)],
..Default::default()
};
let biz = DescriptorProto {
name: Some("Biz".into()),
enum_type: vec![status_enum],
..Default::default()
};
let inner = DescriptorProto {
name: Some("Inner".into()),
field: vec![
FieldDescriptorProto {
name: Some("direct".into()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".a.v1.Biz.Status".into()),
..Default::default()
},
FieldDescriptorProto {
name: Some("via_oneof".into()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_ENUM),
type_name: Some(".a.v1.Biz.Status".into()),
oneof_index: Some(0),
..Default::default()
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("choice".into()),
..Default::default()
}],
..Default::default()
};
let outer = DescriptorProto {
name: Some("Outer".into()),
nested_type: vec![inner],
..Default::default()
};
let svc = DescriptorProto {
name: Some("Svc".into()),
nested_type: vec![outer],
..Default::default()
};
let file_admin = FileDescriptorProto {
name: Some("admin.proto".into()),
package: Some("a.admin.v1".into()),
syntax: Some("proto3".into()),
message_type: vec![svc],
..Default::default()
};
let file_biz = FileDescriptorProto {
name: Some("biz.proto".into()),
package: Some("a.v1".into()),
syntax: Some("proto3".into()),
message_type: vec![biz],
..Default::default()
};
let config = CodeGenConfig {
generate_text: true,
..Default::default()
};
let files = generate(&[file_admin, file_biz], &["admin.proto".into()], &config)
.expect("codegen should succeed");
let content = &joined(&files);
let path_4 = content
.matches("super::super::super::super::v1::biz::Status")
.count();
let path_3 = content
.matches("super::super::super::v1::biz::Status")
.count();
assert_eq!(
path_4, path_3,
"every Inner (nesting=3) reference must use 4 supers, but \
found {path_4} with 4 supers and {path_3} with >=3 supers.\n\
Generated code:\n{content}"
);
assert!(
path_4 > 0,
"expected the cross-package enum reference to appear at least once \
with 4 supers in Inner's module.\nGenerated code:\n{content}"
);
}
#[test]
fn apply_companions_patches_stitcher() {
let mut file = proto3_file("svc/echo.proto");
file.package = Some("svc".to_string());
let mut files = generate(
&[file],
&["svc/echo.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("generate ok");
let stem = proto_path_to_stem("svc/echo.proto");
let companion = GeneratedFile {
name: format!("{stem}.__service.rs"),
package: "svc".to_string(),
kind: GeneratedFileKind::Companion,
content: "pub struct EchoService;".to_string(),
};
apply_companions(&mut files, vec![companion]);
assert!(
files
.iter()
.any(|f| f.kind == GeneratedFileKind::Companion && f.name == "svc.echo.__service.rs"),
"companion file missing from output"
);
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert!(
stitcher
.content
.contains(r#"include!("svc.echo.__service.rs");"#),
"stitcher missing companion include: {}",
stitcher.content
);
syn::parse_file(&stitcher.content).expect("stitcher still parses after apply_companions");
}
#[test]
fn apply_companions_multiple_same_package() {
let mut file = proto3_file("acme/v1/msg.proto");
file.package = Some("acme.v1".to_string());
let mut files = generate(
&[file],
&["acme/v1/msg.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("generate ok");
let stem = proto_path_to_stem("acme/v1/msg.proto");
let companions = vec![
GeneratedFile {
name: format!("{stem}.__service_a.rs"),
package: "acme.v1".to_string(),
kind: GeneratedFileKind::Companion,
content: "pub struct ServiceA;".to_string(),
},
GeneratedFile {
name: format!("{stem}.__service_b.rs"),
package: "acme.v1".to_string(),
kind: GeneratedFileKind::Companion,
content: "pub struct ServiceB;".to_string(),
},
];
apply_companions(&mut files, companions);
let stitcher = files
.iter()
.find(|f| f.kind == GeneratedFileKind::PackageMod)
.expect("stitcher present");
assert!(
stitcher
.content
.contains(r#"include!("acme.v1.msg.__service_a.rs");"#),
"missing service_a include"
);
assert!(
stitcher
.content
.contains(r#"include!("acme.v1.msg.__service_b.rs");"#),
"missing service_b include"
);
assert_eq!(
files
.iter()
.filter(|f| f.kind == GeneratedFileKind::Companion)
.count(),
2
);
syn::parse_file(&stitcher.content).expect("stitcher still parses with two companion includes");
}
#[test]
fn apply_companions_no_matching_package_mod() {
let mut files: Vec<GeneratedFile> = vec![];
let companion = GeneratedFile {
name: "orphan.__service.rs".to_string(),
package: "orphan".to_string(),
kind: GeneratedFileKind::Companion,
content: "pub struct Orphan;".to_string(),
};
apply_companions(&mut files, vec![companion]);
assert_eq!(files.len(), 1);
assert_eq!(files[0].kind, GeneratedFileKind::Companion);
}
#[test]
fn apply_companions_empty_is_noop() {
let mut file = proto3_file("svc/echo.proto");
file.package = Some("svc".to_string());
let mut files = generate(
&[file],
&["svc/echo.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("generate ok");
let before: Vec<String> = files.iter().map(|f| f.content.clone()).collect();
apply_companions(&mut files, vec![]);
let after: Vec<String> = files.iter().map(|f| f.content.clone()).collect();
assert_eq!(before, after, "empty companions list must not mutate files");
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "contains a character that would break")]
fn apply_companions_rejects_unsafe_name() {
let mut files: Vec<GeneratedFile> = vec![];
apply_companions(
&mut files,
vec![GeneratedFile {
name: r#"bad".rs"#.to_string(),
package: "svc".to_string(),
kind: GeneratedFileKind::Companion,
content: String::new(),
}],
);
}
#[test]
fn apply_companions_file_per_package() {
let mut file = proto3_file("svc/msg.proto");
file.package = Some("svc".to_string());
let config = CodeGenConfig {
file_per_package: true,
..Default::default()
};
let mut files =
generate(&[file], &["svc/msg.proto".to_string()], &config).expect("generate ok");
let companion = GeneratedFile {
name: "svc.msg.__service.rs".to_string(),
package: "svc".to_string(),
kind: GeneratedFileKind::Companion,
content: "pub struct MsgService;".to_string(),
};
apply_companions(&mut files, vec![companion]);
let pkg_file = &files[0];
assert!(
pkg_file
.content
.contains(r#"include!("svc.msg.__service.rs");"#),
"file_per_package stitcher missing companion include"
);
syn::parse_file(&pkg_file.content).expect("file_per_package output still parses");
}