mod common;
use common::library_sources;
use common::rust_source::{parse_items, Item};
use std::collections::{BTreeMap, BTreeSet};
const EXCEPTIONS: &[(&str, &str)] = &[(
"ResponseParameters",
"Exists privately in client.rs as the error-response wrapper. Its contents \
already reach callers through Error::Api, so a public type would add \
surface for nothing.",
)];
#[test]
fn every_spec_type_exists() {
let spec = common::load();
let items = parse_items();
let mut covered: BTreeSet<String> = items
.iter()
.filter(|(_, item)| item.public)
.map(|(name, _)| name.clone())
.collect();
for (base, members) in &spec.unions {
let Some(item) = items.get(base) else {
continue;
};
for member in members {
let short = member.strip_prefix(base).unwrap_or(member);
if item.variants.iter().any(|v| {
v.eq_ignore_ascii_case(short)
|| v.eq_ignore_ascii_case(member)
|| short.to_lowercase().starts_with(&v.to_lowercase())
}) {
covered.insert(member.clone());
}
}
}
let excepted: BTreeSet<&str> = EXCEPTIONS.iter().map(|(n, _)| *n).collect();
let missing: Vec<&String> = spec
.types
.keys()
.filter(|t| !covered.contains(*t) && !excepted.contains(t.as_str()))
.collect();
assert!(
missing.is_empty(),
"{} spec type(s) have no Rust counterpart:\n {:?}",
missing.len(),
missing
);
}
#[test]
fn every_spec_field_exists() {
let spec = common::load();
let items = parse_items();
let mut missing = Vec::new();
let mut checked = 0;
for (type_name, spec_fields) in &spec.types {
let Some(item) = items.get(type_name) else {
continue; };
if item.is_enum || item.fields.values().any(|f| f.flattened) {
continue;
}
for field in spec_fields.keys() {
if item.tag.as_deref() == Some(field.as_str())
|| matches!(field.as_str(), "type" | "status" | "source")
{
continue;
}
checked += 1;
if !item.fields.contains_key(field) {
missing.push(format!(" {type_name}.{field}"));
}
}
}
assert!(
checked > 1200,
"only {checked} fields were compared; the parser is not seeing the surface"
);
assert!(
missing.is_empty(),
"{} spec field(s) are missing from their Rust type:\n{}",
missing.len(),
missing.join("\n")
);
}
#[test]
fn every_spec_method_exists() {
let spec = common::load();
let client = library_sources()
.into_iter()
.find(|(p, _)| p.ends_with("client.rs"))
.map(|(_, t)| t)
.expect("client.rs is in the workspace");
let missing: Vec<&String> = spec
.methods
.keys()
.filter(|m| !client.contains(&format!("/// Calls `{m}`")))
.collect();
assert!(
missing.is_empty(),
"{} spec method(s) have no BotClient entry point:\n {:?}",
missing.len(),
missing
);
}
fn builder_parameters(items: &BTreeMap<String, Item>, method: &str) -> BTreeSet<String> {
fn collect(
items: &BTreeMap<String, Item>,
name: &str,
out: &mut BTreeSet<String>,
depth: usize,
) {
if depth > 3 {
return;
}
let Some(item) = items.get(name) else { return };
for (field, info) in &item.fields {
let inner = info
.ty
.trim_start_matches("Option<")
.trim_end_matches('>')
.trim();
if info.flattened || field == "opts" || field == "params" {
collect(items, inner, out, depth + 1);
if let Some(target) = items.get(inner) {
if target.is_enum {
for variant in &target.variants {
collect(items, variant, out, depth + 1);
}
}
}
} else if field != "client" {
out.insert(field.clone());
}
}
}
let capitalised = format!("{}{}", method[..1].to_uppercase(), &method[1..]);
let mut out = BTreeSet::new();
collect(items, &format!("{capitalised}Params"), &mut out, 0);
collect(items, &capitalised, &mut out, 0);
out
}
const MACRO_GENERATED: &[&str] = &[
"closeForumTopic",
"closeGeneralForumTopic",
"deleteForumTopic",
"deleteStickerFromSet",
"deleteStickerSet",
"hideGeneralForumTopic",
"reopenForumTopic",
"reopenGeneralForumTopic",
"sendAnimation",
"sendAudio",
"sendDocument",
"sendSticker",
"sendVideo",
"sendVideoNote",
"sendVoice",
"setStickerPositionInSet",
"setStickerSetTitle",
"unhideGeneralForumTopic",
"unpinAllForumTopicMessages",
"unpinAllGeneralForumTopicMessages",
];
const CONSTRUCTOR_PARAMS: &[(&str, &str, &str)] = &[(
"getBusinessAccountStarBalance",
"business_connection_id",
"Taken as a required constructor argument through the shared \
BizConnectionIdParams, so it never appears in a getBusinessAccountStarBalance-named struct.",
)];
#[test]
fn every_spec_parameter_exists() {
let spec = common::load();
let items = parse_items();
let mut missing = Vec::new();
let mut checked = 0;
let mut unparsed = Vec::new();
for (method, spec_params) in &spec.methods {
let builder = format!("{}{}Params", method[..1].to_uppercase(), &method[1..]);
if !items.contains_key(&builder) && !items.contains_key(&builder[..builder.len() - 6]) {
unparsed.push(method.clone());
continue;
}
let declared = builder_parameters(&items, method);
for param in spec_params.keys() {
if CONSTRUCTOR_PARAMS
.iter()
.any(|(m, p, _)| m == method && p == param)
{
continue;
}
checked += 1;
if !declared.contains(param) {
missing.push(format!(" {method}.{param}"));
}
}
}
let unexpected: Vec<&String> = unparsed
.iter()
.filter(|m| !MACRO_GENERATED.contains(&m.as_str()))
.collect();
assert!(
unexpected.is_empty(),
"{} method(s) have no parsable builder and are not on the macro-generated \
list, so their parameters were never compared against the spec:\n {}",
unexpected.len(),
unexpected
.iter()
.map(|m| m.as_str())
.collect::<Vec<_>>()
.join("\n ")
);
let now_parsable: Vec<&&str> = MACRO_GENERATED
.iter()
.filter(|m| !unparsed.iter().any(|u| u == *m))
.collect();
assert!(
now_parsable.is_empty(),
"{} method(s) on the macro-generated list are now parsable — remove them \
so they are checked here rather than only by the wire sweep: {now_parsable:?}",
now_parsable.len()
);
assert!(
checked > 400,
"only {checked} parameters were compared; the parser is not seeing the builders"
);
assert!(
missing.is_empty(),
"{} spec parameter(s) are missing from their builder:\n{}",
missing.len(),
missing.join("\n")
);
}
#[test]
fn every_documented_exception_still_applies() {
let spec = common::load();
let items = parse_items();
for (name, reason) in EXCEPTIONS {
assert!(
spec.types.contains_key(*name),
"`{name}` is no longer a spec type, so this exception is stale — \
remove it. Its reason was: {reason}"
);
assert!(
!items.get(*name).is_some_and(|item| item.public),
"`{name}` now exists as a public type, so the exception is obsolete — \
remove it and let the coverage test cover it"
);
}
for (method, param, reason) in CONSTRUCTOR_PARAMS {
let spec_method = spec.methods.get(*method).unwrap_or_else(|| {
panic!("`{method}` is no longer a spec method; drop this exception")
});
assert!(
spec_method.contains_key(*param),
"`{method}` no longer takes `{param}`, so this exception is stale — \
remove it. Its reason was: {reason}"
);
}
}