use super::*;
#[test]
fn extracts_typed_proto3_rpc_and_streaming_with_exact_spans() {
let source = concat!(
"syntax = \"proto3\";\n",
"package shop.v1;\n",
"message Outer { message Request {} }\n",
"message Reply {}\n",
"service Inventory { rpc Watch(stream Outer.Request) returns (stream Reply); }\n",
);
let facts = extract(source);
assert!(facts.diagnostics.is_empty());
assert!(
!facts.contracts.is_empty(),
"advertised valid proto3 cannot fall back to empty facts"
);
assert!(facts.contracts.iter().any(|fact| {
matches!(
fact.kind,
ContractKind::ProtobufRpc {
ref input,
ref output,
client_streaming: true,
server_streaming: true,
} if input == "Outer.Request" && output == "Reply"
) && &source[fact.span.start..fact.span.end] == "Watch"
}));
assert!(facts.contracts.iter().any(|fact| {
fact.name == "Outer.Request"
&& fact.owner.as_deref() == Some("Outer")
&& fact.kind == ContractKind::ProtobufMessage
}));
}
#[test]
fn extracts_proto2_and_supported_editions_service_contracts() {
for (source, option_import) in [
(
concat!(
"syntax = \"proto2\";\n",
"message Request { optional string id = 1; }\n",
"message Reply {}\n",
"service Legacy { rpc Get(Request) returns (Reply); }\n",
),
None,
),
(
concat!(
"edition = \"2023\";\n",
"message Request { string id = 1; }\n",
"message Reply {}\n",
"service EditionApi { rpc Get(Request) returns (Reply); }\n",
),
None,
),
(
concat!(
"edition = \"2024\";\n",
"import option \"custom_options.proto\";\n",
"message Request { string id = 1; }\n",
"message Reply {}\n",
"service EditionApi { rpc Get(Request) returns (Reply); }\n",
),
Some("custom_options.proto"),
),
] {
let facts = extract(source);
assert!(facts.diagnostics.is_empty(), "{facts:?}");
assert!(facts.contracts.iter().any(|fact| {
matches!(fact.kind, ContractKind::ProtobufRpc { .. }) && fact.name == "Get"
}));
if let Some(option_import) = option_import {
let import = facts
.imports
.iter()
.find(|import| import.specifier == option_import)
.expect("Edition 2024 option import");
assert!(!import.reexport);
assert_eq!(
source[import.span.start..import.span.end].trim_matches(['"', '\'']),
option_import
);
}
}
}
#[test]
fn invalid_dialects_fail_closed_with_exact_diagnostics() {
for source in [
"syntax = \"proto1\"; message Legacy {}",
"edition = \"future\"; message Legacy {}",
"edition = \"2022\"; message Legacy {}",
"edition = \"2026\"; message Legacy {}",
"package misplaced; syntax = \"proto3\"; message Legacy {}",
"syntax = \"proto3\"; edition = \"2023\"; message Legacy {}",
] {
let facts = extract(source);
assert!(facts.contracts.is_empty());
assert_eq!(facts.diagnostics.len(), 1);
assert_eq!(facts.diagnostics[0].code, "protobuf.invalid_dialect");
let span = facts.diagnostics[0].span;
assert_eq!((span.line, span.column), (1, 1));
assert!(!source[span.start..span.end].is_empty());
}
}
#[test]
fn field_names_that_match_declaration_keywords_keep_earlier_rpcs() {
let source = concat!(
"syntax = \"proto3\";\n",
"package demo;\n",
"import \"google/protobuf/empty.proto\";\n",
"service Test {\n",
" rpc SayHello (HelloRequest) returns (HelloReply) {}\n",
"}\n",
"message HelloReply {\n",
" string message = 1;\n",
" string service = 2;\n",
"}\n",
"enum Status {\n",
" package = 0;\n",
" import = 1;\n",
"}\n",
"message Log {\n",
" enum Kind { service = 0; }\n",
" message Nested { string enum = 1; }\n",
" string message = 5;\n",
"}\n",
);
let facts = extract(source);
assert!(facts.diagnostics.is_empty(), "{facts:?}");
assert!(facts.contracts.iter().any(|fact| {
matches!(fact.kind, ContractKind::ProtobufRpc { .. }) && fact.name == "SayHello"
}));
assert!(facts.contracts.iter().any(|fact| {
fact.name == "Log.Nested"
&& fact.owner.as_deref() == Some("Log")
&& fact.kind == ContractKind::ProtobufMessage
}));
assert_eq!(
facts
.contracts
.iter()
.filter(|fact| fact.kind == ContractKind::ProtobufPackage)
.count(),
1
);
assert!(!facts.contracts.iter().any(|fact| fact.name == "message"));
}
#[test]
fn malformed_supported_dialects_fail_closed() {
for source in [
"syntax = \"proto3\"; service Broken {",
"syntax = \"proto3\"; package bad message X {};",
"syntax = \"proto3\"; service Bad { rpc Call(Request) (Reply); }",
] {
let facts = extract(source);
assert!(facts.contracts.is_empty());
assert_eq!(facts.diagnostics.len(), 1);
assert_eq!(facts.diagnostics[0].code, "protobuf.syntax_error");
let span = facts.diagnostics[0].span;
assert!(
span.end > span.start,
"diagnostic must identify the exact offending token"
);
assert!(!source[span.start..span.end].is_empty());
}
}