use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
const DB_ERROR_VARIANTS: usize = 41;
fn repo() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn binding(file: &str) -> Option<String> {
let path = repo()
.join("bindings")
.join("python")
.join("src")
.join(file);
if !path.exists() {
println!(
"bindings/python/src/{file} is absent, so there is no binding to \
disagree with the ledger. This is the published-tarball case, not \
a skipped check."
);
return None;
}
Some(std::fs::read_to_string(&path).expect("the binding source is valid utf-8"))
}
fn enum_body(rel: &str, name: &str) -> String {
let src = std::fs::read_to_string(repo().join(rel)).expect("valid utf-8");
let decl = format!("pub enum {name} {{");
let after = src
.split_once(&decl)
.unwrap_or_else(|| panic!("`{decl}` not found in {rel}"))
.1;
let mut depth = 1usize;
for (i, ch) in after.char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return after[..i].to_string();
}
}
_ => {}
}
}
panic!("unbalanced braces in `{name}`");
}
fn variant_names(body: &str) -> Vec<String> {
let mut names = Vec::new();
let mut attr_depth = 0i32;
for line in body.lines() {
let stripped = line.trim();
if attr_depth == 0 && stripped.starts_with("#[") {
attr_depth = 1;
}
if attr_depth > 0 {
attr_depth += stripped.matches('(').count() as i32;
attr_depth -= stripped.matches(')').count() as i32;
if stripped.ends_with(']') && attr_depth <= 1 {
attr_depth = 0;
}
continue;
}
if stripped.is_empty() || stripped.starts_with("//") {
continue;
}
let name: String = stripped
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if name.starts_with(|c: char| c.is_ascii_uppercase()) {
names.push(name);
}
}
names
}
fn without_comments(src: &str) -> String {
src.lines()
.filter(|l| !l.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
fn class_of_variant(src: &str) -> BTreeMap<String, String> {
let build = src
.split_once("fn build(")
.expect("`fn build` is the mapping")
.1;
let mut class_of = BTreeMap::new();
let starts: Vec<usize> = build.match_indices("DbError::").map(|(i, _)| i).collect();
for (n, &start) in starts.iter().enumerate() {
let end = starts.get(n + 1).copied().unwrap_or(build.len());
let arm = &build[start..end];
let variant: String = arm["DbError::".len()..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
let class = arm
.split_once("raise::<")
.map(|(_, rest)| {
rest.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>()
})
.unwrap_or_else(|| panic!("`DbError::{variant}`'s arm raises nothing"));
class_of.insert(variant, class);
}
class_of
}
fn mentions(src: &str, enum_name: &str) -> BTreeMap<String, usize> {
let needle = format!("{enum_name}::");
let mut out = BTreeMap::new();
for (i, _) in src.match_indices(&needle) {
let preceded = src[..i]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_');
if preceded {
continue;
}
let rest = &src[i + needle.len()..];
let name: String = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if name.starts_with(|c: char| c.is_ascii_uppercase()) {
*out.entry(name).or_insert(0) += 1;
}
}
out
}
fn declared_db_error_variants() -> Vec<String> {
variant_names(&enum_body("src/error.rs", "DbError"))
}
#[test]
fn the_db_error_variant_count_is_the_one_recorded_here() {
let declared = declared_db_error_variants();
assert_eq!(
declared.len(),
DB_ERROR_VARIANTS,
"`DbError` has {} variants and this file says {DB_ERROR_VARIANTS}. If a \
variant was added, it needs an arm in bindings/python/src/errors.rs, an \
entry in that crate's `DB_ERROR_VARIANTS`, a row in \
tests_py/test_errors.py's `EXPECTED`, and this number updated. \
Declared: {declared:?}",
declared.len()
);
}
#[test]
fn every_db_error_variant_has_an_arm_in_the_binding() {
let Some(src) = binding("errors.rs") else {
return;
};
let declared: BTreeSet<String> = declared_db_error_variants().into_iter().collect();
let matched: BTreeSet<String> = mentions(&without_comments(&src), "DbError")
.into_keys()
.collect();
let missing: Vec<_> = declared.difference(&matched).collect();
assert!(
missing.is_empty(),
"these `DbError` variants have no arm in bindings/python/src/errors.rs, \
so they reach Python as the bare `MacrameError` the wildcard arm \
raises: {missing:?}. Until 0.13.33 this was a compile error; \
`#[non_exhaustive]` traded that away and this test is the replacement \
(D-207)."
);
let stale: Vec<_> = matched.difference(&declared).collect();
assert!(
stale.is_empty(),
"bindings/python/src/errors.rs names `DbError` variants that no longer \
exist in src/error.rs: {stale:?}"
);
}
#[test]
fn every_db_error_variant_reaches_a_class_of_its_own() {
let Some(src) = binding("errors.rs") else {
return;
};
let class_of = class_of_variant(&without_comments(&src));
let mut by_class: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (variant, class) in &class_of {
by_class.entry(class).or_default().push(variant);
}
let shared: Vec<_> = by_class.iter().filter(|(_, v)| v.len() > 1).collect();
assert!(
shared.is_empty(),
"two or more variants share an exception class: {shared:?}. Sharing is \
the quiet form of flattening — a caller who catches the class is back \
to reading the message to find out what happened."
);
}
#[test]
fn every_db_error_variant_is_sampled_for_the_mapping_tests() {
let Some(src) = binding("testing.rs") else {
return;
};
let declared: BTreeSet<String> = declared_db_error_variants().into_iter().collect();
let list = src
.split_once("DB_ERROR_VARIANTS: &[&str] = &[")
.expect("the sample table")
.1
.split_once("];")
.expect("the sample table ends")
.0;
let sampled: BTreeSet<String> = list
.split('"')
.skip(1)
.step_by(2)
.map(str::to_string)
.collect();
assert_eq!(
declared, sampled,
"the binding's `DB_ERROR_VARIANTS` and `src/error.rs` disagree. A \
variant that is mapped but never sampled has a plausible-looking arm \
nothing has ever executed."
);
}
const CONVERTED: &[(&str, &str, &str)] = &[
("src/graph/builder.rs", "AttributeMode", "types.rs"),
(
"src/graph/vector_filter.rs",
"VectorFilterStrategy",
"vector.rs",
),
];
#[test]
fn every_variant_of_a_converted_domain_enum_is_converted_everywhere_its_peers_are() {
for &(rel, name, file) in CONVERTED {
let Some(src) = binding(file) else {
return;
};
let declared = variant_names(&enum_body(rel, name));
let counted = mentions(&without_comments(&src), name);
let stale: Vec<_> = counted.keys().filter(|k| !declared.contains(k)).collect();
assert!(
stale.is_empty(),
"bindings/python/src/{file} names `{name}` variants that no longer \
exist in {rel}: {stale:?}"
);
let per: BTreeSet<usize> = declared
.iter()
.map(|v| counted.get(v).copied().unwrap_or(0))
.collect();
assert!(
per.len() == 1 && !per.contains(&0),
"`{name}` is converted unevenly in bindings/python/src/{file}: \
{counted:?}. Every variant should appear once per conversion site. \
The wildcard arms there are `unreachable!`, so a variant that is \
short an arm is a panic in a released wheel — this test is where \
that is supposed to be caught."
);
}
}
#[test]
fn the_variant_parser_survives_a_wrapped_attribute() {
let body = r#"
/// Doc.
#[error(
"refused; open with FutureStampPolicy::Allow to inspect it, then \
Tolerance(d) to carry on"
)]
RealVariant { field: String },
#[error("plain")]
Another,
"#;
assert_eq!(variant_names(body), vec!["RealVariant", "Another"]);
}
#[test]
fn the_kind_of_a_variant_and_the_base_of_its_exception_agree() {
let Some(errors_rs) = binding("errors.rs") else {
return;
};
let base_of_kind: BTreeMap<&str, &str> = [
("Integrity", "IntegrityError"),
("Validation", "ValidationError"),
("Vector", "VectorError"),
("Temporal", "TemporalError"),
("Writer", "WriterError"),
("Budget", "BudgetError"),
("Branch", "BranchError"),
("Cancelled", "MacrameError"),
("Diagnostic", "MacrameError"),
("Engine", "MacrameError"),
("Migration", "MacrameError"),
("NotFound", "MacrameError"),
]
.into_iter()
.collect();
let kind_of = kind_arms();
let variants = variant_names(&enum_body("src/error.rs", "DbError"));
assert_eq!(
kind_of.len(),
variants.len(),
"`DbError::kind` classifies {} variants and the enum has {}. The \
compiler enforces the arms, so this is the parser losing one -- fix \
`kind_arms`, not `kind()`.",
kind_of.len(),
variants.len()
);
let mut base_of_class: BTreeMap<String, String> = BTreeMap::new();
for (i, _) in errors_rs.match_indices("create_exception!(") {
let rest = &errors_rs[i + "create_exception!(".len()..];
let head: Vec<String> = rest
.split(',')
.take(3)
.map(|f| f.trim().to_string())
.collect();
if head.len() == 3 && head[0] == "macrame" {
base_of_class.insert(head[1].clone(), head[2].clone());
}
}
let class_of = class_of_variant(&without_comments(&errors_rs));
let mut disagreements = Vec::new();
for variant in &variants {
let (Some(kind), Some(class)) = (kind_of.get(variant), class_of.get(variant)) else {
continue; };
let expected = base_of_kind
.get(kind.as_str())
.unwrap_or_else(|| panic!("`ErrorKind::{kind}` has no row in this test's table"));
let actual = base_of_class
.get(class)
.unwrap_or_else(|| panic!("`{class}` is raised but never declared"));
if actual != expected {
disagreements.push(format!(
"{variant}: kind() says {kind} (-> {expected}), {class} derives from {actual}"
));
}
}
assert!(
disagreements.is_empty(),
"one taxonomy, two answers -- the Rust classification and the Python \
hierarchy disagree about {} variant(s):\n {}",
disagreements.len(),
disagreements.join("\n ")
);
let used: BTreeSet<&String> = kind_of.values().collect();
let unused: Vec<&&str> = base_of_kind
.keys()
.filter(|k| !used.contains(&k.to_string()))
.collect();
assert!(
unused.is_empty(),
"`ErrorKind` has {} variant(s) no `DbError` produces: {unused:?}",
unused.len()
);
}
fn kind_arms() -> BTreeMap<String, String> {
let src = std::fs::read_to_string(repo().join("src/error.rs")).expect("valid utf-8");
let body = src
.split_once("pub fn kind(&self) -> ErrorKind {")
.expect("`DbError::kind` is where C-3 put it")
.1;
let mut out = BTreeMap::new();
let mut pending: Vec<String> = Vec::new();
for line in without_comments(body).lines() {
let line = line.trim();
if line == "}" && pending.is_empty() && !out.is_empty() {
break;
}
for (i, _) in line.match_indices("Self::") {
let name: String = line[i + "Self::".len()..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
pending.push(name);
}
}
if let Some((_, tail)) = line.split_once("=> ErrorKind::") {
let kind: String = tail
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
for name in pending.drain(..) {
out.insert(name, kind.clone());
}
}
}
out
}