cu_profiler_core/
bench.rs1use serde::{Deserialize, Serialize};
26
27use crate::error::{Error, Result};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[serde(deny_unknown_fields)]
32pub struct BenchPlan {
33 #[serde(default, rename = "instruction")]
35 pub instructions: Vec<InstructionFixture>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct InstructionFixture {
42 pub scenario: String,
44 pub program_id: String,
46 #[serde(default)]
48 pub data: String,
49 #[serde(default, rename = "account")]
51 pub accounts: Vec<AccountFixture>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct AccountFixture {
58 pub pubkey: String,
60 #[serde(default)]
62 pub signer: bool,
63 #[serde(default)]
65 pub writable: bool,
66 #[serde(default)]
68 pub lamports: u64,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub owner: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub data: Option<String>,
75}
76
77impl BenchPlan {
78 pub fn from_toml(s: &str) -> Result<Self> {
84 let plan: BenchPlan = toml::from_str(s).map_err(|e| Error::Config(e.to_string()))?;
85 plan.validate()?;
86 Ok(plan)
87 }
88
89 pub fn validate(&self) -> Result<()> {
94 if self.instructions.is_empty() {
95 return Err(Error::Config(
96 "bench plan has no `[[instruction]]` entries".to_string(),
97 ));
98 }
99 for ix in &self.instructions {
100 ix.validate()?;
101 }
102 Ok(())
103 }
104}
105
106impl InstructionFixture {
107 fn validate(&self) -> Result<()> {
108 let ctx = format!("instruction `{}`", self.scenario);
109 if self.scenario.is_empty() {
110 return Err(Error::Config(
111 "an instruction has an empty `scenario`".to_string(),
112 ));
113 }
114 validate_base58(&self.program_id, &format!("{ctx}: program_id"))?;
115 validate_hex(&self.data, &format!("{ctx}: data"))?;
116 for acc in &self.accounts {
117 validate_base58(&acc.pubkey, &format!("{ctx}: account pubkey"))?;
118 if let Some(owner) = &acc.owner {
119 validate_base58(owner, &format!("{ctx}: account owner"))?;
120 }
121 if let Some(data) = &acc.data {
122 validate_hex(data, &format!("{ctx}: account data"))?;
123 }
124 }
125 Ok(())
126 }
127}
128
129const BASE58_ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
131
132fn validate_base58(s: &str, what: &str) -> Result<()> {
137 if !(32..=44).contains(&s.len()) {
138 return Err(Error::Config(format!(
139 "{what}: `{s}` is not a 32-byte base58 address (length {})",
140 s.len()
141 )));
142 }
143 if let Some(bad) = s.bytes().find(|b| !BASE58_ALPHABET.contains(b)) {
144 return Err(Error::Config(format!(
145 "{what}: `{s}` contains a non-base58 character `{}`",
146 bad as char
147 )));
148 }
149 Ok(())
150}
151
152fn validate_hex(s: &str, what: &str) -> Result<()> {
154 if s.len() % 2 != 0 {
155 return Err(Error::Config(format!(
156 "{what}: hex string has an odd length ({})",
157 s.len()
158 )));
159 }
160 if let Some(bad) = s.bytes().find(|b| !b.is_ascii_hexdigit()) {
161 return Err(Error::Config(format!(
162 "{what}: `{s}` contains a non-hex character `{}`",
163 bad as char
164 )));
165 }
166 Ok(())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 const SYS: &str = "11111111111111111111111111111111";
174
175 fn plan_toml() -> String {
176 format!(
177 "[[instruction]]\nscenario=\"swap\"\nprogram_id=\"{SYS}\"\ndata=\"01ab\"\n\
178 [[instruction.account]]\npubkey=\"{SYS}\"\nsigner=true\nwritable=true\nlamports=1000000\n"
179 )
180 }
181
182 #[test]
183 fn parses_and_validates_a_plan() {
184 let plan = BenchPlan::from_toml(&plan_toml()).unwrap();
185 assert_eq!(plan.instructions.len(), 1);
186 let ix = &plan.instructions[0];
187 assert_eq!(ix.scenario, "swap");
188 assert_eq!(ix.data, "01ab");
189 assert_eq!(ix.accounts.len(), 1);
190 assert!(ix.accounts[0].signer && ix.accounts[0].writable);
191 }
192
193 #[test]
194 fn empty_plan_is_rejected() {
195 assert!(BenchPlan::from_toml("").is_err());
196 }
197
198 #[test]
199 fn rejects_unknown_keys() {
200 let toml = format!("[[instruction]]\nscenario=\"s\"\nprogram_id=\"{SYS}\"\nbogus=1\n");
201 assert!(BenchPlan::from_toml(&toml).is_err());
202 }
203
204 #[test]
205 fn rejects_bad_base58_program_id() {
206 let toml = "[[instruction]]\nscenario=\"s\"\nprogram_id=\"not-base58-0OIl\"\n";
207 let err = BenchPlan::from_toml(toml).unwrap_err().to_string();
208 assert!(err.contains("base58"), "{err}");
209 }
210
211 #[test]
212 fn rejects_odd_and_nonhex_data() {
213 let odd = format!("[[instruction]]\nscenario=\"s\"\nprogram_id=\"{SYS}\"\ndata=\"abc\"\n");
214 assert!(
215 BenchPlan::from_toml(&odd)
216 .unwrap_err()
217 .to_string()
218 .contains("odd")
219 );
220 let nonhex =
221 format!("[[instruction]]\nscenario=\"s\"\nprogram_id=\"{SYS}\"\ndata=\"zz\"\n");
222 assert!(
223 BenchPlan::from_toml(&nonhex)
224 .unwrap_err()
225 .to_string()
226 .contains("non-hex")
227 );
228 }
229}