use super::*;
use crate::error::Error;
use std::collections::BTreeMap;
fn error_schema_src() -> &'static str {
include_str!("error_schema.rs")
}
fn backticked(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut i = 0;
while let Some(open) = s[i..].find('`') {
let open = i + open;
let Some(close) = s[open + 1..].find('`') else {
break;
};
let close = open + 1 + close;
let token = s[open + 1..close].trim();
if !token.is_empty() {
out.push(token);
}
i = close + 1;
}
out
}
fn split_commentary(cell: &str) -> Result<(String, String), String> {
let mut body = String::new();
let mut commentary = String::new();
let mut depth = 0usize;
for c in cell.chars() {
match c {
'(' => {
depth += 1;
commentary.push(c);
}
')' => {
depth = depth
.checked_sub(1)
.ok_or_else(|| format!("unbalanced `)` in `Maps from` cell: {cell}"))?;
commentary.push(c);
}
_ if depth > 0 => commentary.push(c),
_ => body.push(c),
}
}
if depth != 0 {
return Err(format!("unclosed `(` in `Maps from` cell: {cell}"));
}
Ok((body, commentary))
}
const FORBIDDEN_TABLE_CLAIMS: [&str; 5] = [
"catch-all",
"catchall",
"future variant",
"other variant",
"all remaining",
];
fn parse_maps_from(cell: &str) -> Result<Vec<String>, String> {
let (body, commentary) = split_commentary(cell)?;
let lowered = format!("{body} {commentary}").to_ascii_lowercase();
for claim in FORBIDDEN_TABLE_CLAIMS {
if lowered.contains(claim) {
return Err(format!(
"`Maps from` cell claims {claim:?}, which classify() does not do — it \
has no catch-all arm, so an uncategorised variant is a compile error, \
not an `Other`: {cell}"
));
}
}
let mut out = Vec::new();
for item in body.split(',') {
let item = item.trim();
if item.is_empty() {
continue; }
let tokens = backticked(item);
let ident = match tokens.first() {
Some(first) if tokens.len() == 1 && item == format!("`{first}`") => *first,
_ => {
return Err(format!(
"`Maps from` item {item:?} is not a backticked `Error` variant name \
(put commentary in parentheses or in the prose below the table): {cell}"
))
}
};
if !ident.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|| !ident.starts_with(|c: char| c.is_ascii_uppercase())
{
return Err(format!(
"`Maps from` item {ident:?} is not an `Error` variant identifier: {cell}"
));
}
out.push(ident.to_string());
}
Ok(out)
}
fn documented_taxonomy() -> Vec<(String, String, Vec<String>)> {
let mut rows: Vec<(String, String, Vec<String>)> = Vec::new();
let mut past_separator = false;
for line in error_schema_src().lines() {
let Some(doc) = line.strip_prefix("//!") else {
continue;
};
let doc = doc.trim();
if !doc.starts_with('|') {
if past_separator && !rows.is_empty() {
break;
}
continue;
}
if doc.contains("|---") {
past_separator = true;
continue;
}
if !past_separator {
continue;
}
let cells: Vec<&str> = doc.trim_matches('|').split('|').collect();
assert_eq!(
cells.len(),
3,
"taxonomy table row must have exactly 3 columns: {doc}"
);
let variant = backticked(cells[0]);
let label = backticked(cells[1]);
let mapped: Vec<String> = parse_maps_from(cells[2])
.unwrap_or_else(|why| panic!("taxonomy table row is not parseable: {why}"));
if variant.is_empty() {
let last = rows
.last_mut()
.expect("a continuation row must follow a row");
last.2.extend(mapped);
continue;
}
assert_eq!(
variant.len(),
1,
"column 1 must name exactly one category variant: {doc}"
);
assert_eq!(
label.len(),
1,
"column 2 must name exactly one as_str() label: {doc}"
);
rows.push((variant[0].to_string(), label[0].to_string(), mapped));
}
assert!(
rows.len() > 1,
"the taxonomy table must have been found and parsed"
);
rows
}
fn error_src() -> &'static str {
include_str!("../error.rs")
}
const WASM_VARIANTS_COMPILED: bool = cfg!(target_arch = "wasm32");
fn declared_error_variants() -> BTreeMap<String, bool> {
const HEAD: &str = "pub enum Error {";
let src = error_src();
let start = src
.find(HEAD)
.expect("error.rs must declare `pub enum Error`");
let body = &src[start + HEAD.len()..];
let mut out = BTreeMap::new();
let mut wasm_gated = false;
for line in body.lines() {
if line == "}" {
break; }
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if indent != 4 {
continue; }
if trimmed.starts_with("#[cfg(target_arch = \"wasm32\")]") {
wasm_gated = true;
continue;
}
if !trimmed.starts_with(|c: char| c.is_ascii_uppercase()) {
continue; }
let ident: String = trimmed
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
let rest = trimmed[ident.len()..].trim_start();
if !(rest.starts_with('(') || rest.starts_with('{') || rest.starts_with(',')) {
continue;
}
out.insert(ident, wasm_gated);
wasm_gated = false;
}
assert!(
out.len() > 30,
"the Error enum declaration must have been parsed; got {out:?}"
);
out
}
fn parse_classify_arms(body: &str) -> Result<BTreeMap<String, (String, bool)>, String> {
const ARM: &str = "=> ObsErrorCategory::";
const CFG_WASM: &str = "#[cfg(target_arch = \"wasm32\")]";
let mut out: BTreeMap<String, (String, bool)> = BTreeMap::new();
let mut prev_end = body
.find("match ")
.and_then(|i| body[i..].find('{').map(|j| i + j + 1))
.ok_or_else(|| "classify()'s body must contain a `match … {` opener".to_string())?;
for (idx, _) in body.match_indices(ARM) {
if idx < prev_end {
continue;
}
let patterns = &body[prev_end..idx];
let after = &body[idx + ARM.len()..];
let category: String = after
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
if category.is_empty() {
return Err("an arm must name an ObsErrorCategory variant".to_string());
}
prev_end = idx + ARM.len() + category.len();
let cleaned: String = patterns
.lines()
.map(|l| l.split("//").next().unwrap_or("").trim())
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join(" ");
let cleaned = cleaned.trim().trim_start_matches(',').trim();
for alt in cleaned.split('|') {
let alt = alt.trim();
if alt.is_empty() {
continue;
}
let (gated, alt) = match alt.strip_prefix(CFG_WASM) {
Some(rest) => (true, rest.trim()),
None => (false, alt),
};
let Some(rest) = alt.strip_prefix("Error::") else {
return Err(format!(
"classify() arm alternative {alt:?} is not an explicit `Error::` \
pattern — a wildcard or named binding would absorb future \
variants silently, which is exactly the drift this guard exists \
to catch (issue #1705)"
));
};
let ident: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
if ident.is_empty() {
return Err(format!("could not read a variant name from {alt:?}"));
}
if let Some(prior) = out.insert(ident.clone(), (category.clone(), gated)) {
if prior.0 != category {
return Err(format!(
"Error::{ident} appears in two classify() arms ({} and {category})",
prior.0
));
}
}
}
}
if out.len() <= 20 {
return Err(format!("classify() must have been parsed; got {out:?}"));
}
Ok(out)
}
fn classify_body() -> &'static str {
let src = error_schema_src();
let start = src
.find("fn classify(")
.expect("classify() must exist in error_schema.rs");
let body = &src[start..];
let end = body
.find("\n}\n")
.expect("classify() must be terminated by a column-0 closing brace");
&body[..end]
}
fn classify_arms() -> BTreeMap<String, (String, bool)> {
match parse_classify_arms(classify_body()) {
Ok(map) => map,
Err(why) => panic!("classify() is not exhaustively enumerable: {why}"),
}
}
fn error_samples() -> Vec<Error> {
let mut samples = vec![
Error::Io(std::io::Error::other("x")),
Error::Serialization {
message: "m".into(),
source: None,
},
Error::Corruption("c".into()),
Error::column_decode("col", "int", 0, Error::Corruption("c".into())),
Error::Schema("s".into()),
Error::CqlParse("q".into()),
Error::InvalidFormat("f".into()),
Error::UnsupportedFormat("f".into()),
Error::UnsupportedVersion {
version: "ma".into(),
floor: "na".into(),
},
Error::UnsupportedCommitLogVersion {
version: 5,
floor: 6,
ceiling: 7,
},
Error::CorruptCommitLogFrame("f".into()),
Error::Timeout("t".into()),
Error::InvalidPath("p".into()),
Error::InvalidState("s".into()),
Error::QueryExecution("q".into()),
Error::QueryTimeout {
operation: "query.execute".into(),
elapsed: std::time::Duration::from_millis(1500),
limit: std::time::Duration::from_millis(1000),
},
Error::ResultTooLarge {
budget_bytes: 1,
estimated_bytes: 2,
rows: 3,
},
Error::InvalidReadPath {
value: "nope".into(),
},
Error::ForcedReadPathUnavailable {
forced: "point",
reason: "r".into(),
},
Error::TypeConversion("t".into()),
Error::Configuration("c".into()),
Error::Storage("s".into()),
Error::Memory("m".into()),
Error::Concurrency("c".into()),
Error::WriteDirLocked { path: "/d".into() },
Error::NotFound("n".into()),
Error::Table("t".into()),
Error::AlreadyExists("a".into()),
Error::InvalidOperation("o".into()),
Error::ConstraintViolation("v".into()),
Error::Transaction("t".into()),
Error::Index("i".into()),
Error::Compaction("c".into()),
Error::Internal("i".into()),
Error::Parse("p".into()),
Error::InvalidInput("i".into()),
Error::UnsupportedQuery("q".into()),
Error::Cancelled,
];
#[cfg(target_arch = "wasm32")]
samples.push(Error::Wasm("w".into()));
samples.sort_by_key(debug_variant_name);
samples
}
fn debug_variant_name(err: &Error) -> String {
format!("{err:?}")
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect()
}
fn measured_categories() -> BTreeMap<String, String> {
error_samples()
.iter()
.map(|e| (debug_variant_name(e), format!("{:?}", classify(e))))
.collect()
}
fn actual_map() -> BTreeMap<String, String> {
let mut out = measured_categories();
if !WASM_VARIANTS_COMPILED {
for (variant, (category, gated)) in classify_arms() {
if gated {
out.insert(variant, category);
}
}
}
out
}
fn documented_map() -> BTreeMap<String, String> {
let mut documented = BTreeMap::new();
for (category, _, mapped) in documented_taxonomy() {
for variant in mapped {
let prior = documented.insert(variant.clone(), category.clone());
assert!(
prior.is_none(),
"Error::{variant} is documented under two categories \
({prior:?} and {category})"
);
}
}
documented
}
#[test]
fn as_str_is_lowercase_and_unique() {
let mut seen = std::collections::HashSet::new();
for c in ObsErrorCategory::ALL {
let s = c.as_str();
assert_eq!(s, s.to_ascii_lowercase());
assert!(seen.insert(s), "duplicate category label {s}");
}
assert_eq!(seen.len(), ObsErrorCategory::ALL.len());
}
#[test]
fn display_matches_as_str() {
for c in ObsErrorCategory::ALL {
assert_eq!(c.to_string(), c.as_str());
}
}
#[test]
fn documented_categories_equal_the_error_category_enum() {
let documented: Vec<(String, String)> = documented_taxonomy()
.into_iter()
.map(|(v, l, _)| (v, l))
.collect();
let actual: Vec<(String, String)> = ObsErrorCategory::ALL
.iter()
.map(|c| (format!("{c:?}"), c.as_str().to_string()))
.collect();
assert_eq!(
documented, actual,
"the error_schema taxonomy table's categories must equal ObsErrorCategory::ALL \
(variant + as_str label, same order)"
);
}
#[test]
fn classify_has_no_catch_all_arm() {
if let Err(why) = parse_classify_arms(classify_body()) {
panic!(
"classify() must stay exhaustively enumerable (no catch-all arm): {why}\n\
The taxonomy guard's completeness comes from the COMPILER refusing to \
build until each new Error variant is named in an arm."
);
}
}
#[test]
fn the_classify_arm_parser_rejects_a_catch_all() {
let mut ok = String::from("fn classify(err: &Error) -> ObsErrorCategory {\n match err {\n");
for n in 0..21 {
ok.push_str(&format!(
" Error::V{n}(_) => ObsErrorCategory::Other,\n"
));
}
let parsed = parse_classify_arms(&ok).expect("an all-explicit body must parse");
assert_eq!(parsed.len(), 21);
assert_eq!(parsed["V7"], ("Other".to_string(), false));
for bad in [
format!("{ok} _ => ObsErrorCategory::Other,\n"),
format!("{ok} Error::VX(_) | _ => ObsErrorCategory::Other,\n"),
format!("{ok} other => ObsErrorCategory::Other,\n"),
] {
let err = parse_classify_arms(&bad)
.expect_err("a catch-all arm must be rejected, not silently parsed");
assert!(
err.contains("not an explicit `Error::` pattern"),
"unexpected rejection reason: {err}"
);
}
}
#[test]
fn declared_error_variants_equal_classify_arms() {
let declared: Vec<String> = declared_error_variants().into_keys().collect();
let arms: Vec<String> = classify_arms().into_keys().collect();
assert_eq!(
declared, arms,
"the Error enum declaration and classify()'s match arms must name the same \
variants (classify() is exhaustive, so any difference is a parse bug in \
one of the two derivations)"
);
}
#[test]
fn every_compiled_error_variant_has_a_constructed_sample() {
let expected: Vec<String> = declared_error_variants()
.into_iter()
.filter(|(_, wasm_gated)| WASM_VARIANTS_COMPILED || !wasm_gated)
.map(|(v, _)| v)
.collect();
let sampled: Vec<String> = error_samples().iter().map(debug_variant_name).collect();
assert_eq!(
sampled, expected,
"error_samples() must hold exactly one constructed value per Error variant \
this target compiles (names are read back from each value's Debug output)"
);
}
#[test]
fn the_maps_from_parser_rejects_prose_that_smuggles_a_behavioural_claim() {
let stale = "`Internal`, `Wasm`, and any future variant (catch-all)";
let why = parse_maps_from(stale).expect_err("the stale `Other` cell must be rejected");
assert!(
why.contains("catch-all"),
"the rejection must name the false claim: {why}"
);
let prose = parse_maps_from("`Internal`, plus whatever else turns up")
.expect_err("an unbacketed item must be rejected");
assert!(
prose.contains("is not a backticked `Error` variant name"),
"unexpected rejection reason: {prose}"
);
assert!(parse_maps_from("`Internal`, `see below`").is_err());
assert!(parse_maps_from("`Internal` and any others").is_err());
assert_eq!(
parse_maps_from("`Cancelled` (issue #2264 — a cooperative abort, never `Io`)")
.expect("parenthetical commentary is allowed"),
vec!["Cancelled".to_string()],
);
assert_eq!(
parse_maps_from("`Internal`, `Wasm` (`wasm32` builds only)")
.expect("a cfg note is allowed"),
vec!["Internal".to_string(), "Wasm".to_string()],
);
assert!(parse_maps_from("`Internal` (oops").is_err());
assert!(parse_maps_from("`Internal`)").is_err());
let rows = documented_taxonomy();
let other = rows
.iter()
.find(|(category, _, _)| category == "Other")
.expect("the taxonomy table must have an `Other` row");
assert!(
other.2.contains(&"Internal".to_string()),
"the `Other` row must still document its variants: {other:?}"
);
}
#[test]
fn every_error_variant_classify_routes_is_documented_in_the_taxonomy_table() {
let classified = actual_map();
let documented = documented_map();
let undocumented: Vec<String> = classified
.iter()
.filter(|(v, _)| !documented.contains_key(v.as_str()))
.map(|(v, c)| format!("Error::{v} -> {c}"))
.collect();
assert!(
undocumented.is_empty(),
"classify() routes Error variants that the error_schema taxonomy table \
does NOT document (add them to the table's `Maps from` column): {undocumented:?}"
);
let phantom: Vec<String> = documented
.iter()
.filter(|(v, _)| !classified.contains_key(v.as_str()))
.map(|(v, c)| format!("Error::{v} (documented under {c})"))
.collect();
assert!(
phantom.is_empty(),
"the error_schema taxonomy table documents Error variants that classify() \
does not route (remove the phantom rows): {phantom:?}"
);
assert_eq!(
classified, documented,
"every Error variant must be documented under the SAME category classify() \
actually assigns it"
);
}
fn independent_expectations() -> Vec<(Error, ObsErrorCategory)> {
use ObsErrorCategory::*;
let mut out = vec![
(Error::from(std::io::Error::other("x")), Io),
(Error::invalid_path("p"), Io),
(Error::Timeout("t".into()), Io),
(Error::serialization("s"), Serialization),
(Error::type_conversion("t"), Serialization),
(Error::corruption("c"), Corruption),
(Error::CorruptCommitLogFrame("f".into()), Corruption),
(
Error::column_decode("col", "int", 0, Error::corruption("c")),
Corruption,
),
(Error::schema("s"), Schema),
(Error::Table("t".into()), Schema),
(Error::parse("p"), Parsing),
(Error::cql_parse("p"), Parsing),
(Error::invalid_format("f"), Parsing),
(Error::unsupported_format("f"), Parsing),
(
Error::UnsupportedVersion {
version: "ma".into(),
floor: "na".into(),
},
Parsing,
),
(
Error::UnsupportedCommitLogVersion {
version: 5,
floor: 6,
ceiling: 7,
},
Parsing,
),
(Error::storage("s"), Storage),
(Error::memory("m"), Storage),
(Error::index("i"), Storage),
(Error::compaction("c"), Storage),
(Error::write_dir_locked("/d"), Storage),
(Error::concurrency("c"), Concurrency),
(Error::transaction("t"), Concurrency),
(Error::constraint_violation("c"), Constraints),
(Error::already_exists("a"), Constraints),
(Error::query_execution("q"), Query),
(Error::unsupported_query("q"), Query),
(Error::invalid_input("i"), Query),
(
Error::ResultTooLarge {
budget_bytes: 1,
estimated_bytes: 2,
rows: 3,
},
Query,
),
(
Error::ForcedReadPathUnavailable {
forced: "point",
reason: "r".into(),
},
Query,
),
(
Error::InvalidReadPath {
value: "nope".into(),
},
Query,
),
(
Error::QueryTimeout {
operation: "query.execute".into(),
elapsed: std::time::Duration::from_millis(1500),
limit: std::time::Duration::from_millis(1000),
},
Timeout,
),
(Error::configuration("c"), Other),
(Error::invalid_state("s"), Other),
(Error::invalid_operation("o"), Other),
(Error::not_found("n"), Other),
(Error::internal("i"), Other),
(Error::Cancelled, Cancelled),
];
out.extend(wasm_expectations());
out
}
fn wasm_expectations() -> Vec<(Error, ObsErrorCategory)> {
#[cfg(target_arch = "wasm32")]
{
vec![(Error::Wasm("w".into()), ObsErrorCategory::Other)]
}
#[cfg(not(target_arch = "wasm32"))]
{
Vec::new()
}
}
#[test]
fn classify_every_error_variant() {
for (err, expected) in independent_expectations() {
let variant = debug_variant_name(&err);
assert_eq!(
err.obs_category(),
expected,
"Error::{variant} must classify as {expected:?}"
);
}
}
#[test]
fn the_independent_category_test_covers_every_variant_the_taxonomy_documents() {
let covered: std::collections::BTreeSet<String> = independent_expectations()
.iter()
.map(|(err, _)| debug_variant_name(err))
.collect();
assert!(
!covered.is_empty(),
"the coverage guard must have a subject — an empty expectation list passes \
vacuously"
);
let missing: Vec<String> = classify_arms()
.into_iter()
.filter(|(variant, (_, wasm_gated))| {
(WASM_VARIANTS_COMPILED || !wasm_gated) && !covered.contains(variant)
})
.map(|(variant, (category, _))| format!("Error::{variant} -> {category}"))
.collect();
assert!(
missing.is_empty(),
"classify() routes variants that the INDEPENDENT hand-written expectation \
list never pins, so nothing asserts their CORRECT category (add them to \
`independent_expectations`): {missing:?}"
);
let routed: std::collections::BTreeSet<String> = classify_arms().into_keys().collect();
let stale: Vec<&String> = covered.iter().filter(|v| !routed.contains(*v)).collect();
assert!(
stale.is_empty(),
"the independent expectation list names variants classify() does not route \
(remove the stale entries): {stale:?}"
);
}