1use std::any::Any;
2
3use crate::cmd::args::CommandArg;
4use crate::cmd::SCENARIOS_SUBCOMMAND;
5use crate::custom_types::CustomTypeSet;
6use crate::{container::ContractError, types, DeployedContractsContainer};
7use anyhow::Result;
8use clap::{ArgMatches, Command};
9use odra::casper_types::{CLTyped, CLValue};
10use odra::schema::NamedCLTyped;
11use odra::{casper_types::bytesrepr::FromBytes, host::HostEnv, prelude::OdraError};
12use thiserror::Error;
13
14use super::OdraCommand;
15
16pub trait Scenario: Any {
22 fn args(&self) -> Vec<CommandArg> {
23 vec![]
24 }
25 fn run(
26 &self,
27 env: &HostEnv,
28 container: &DeployedContractsContainer,
29 args: ScenarioArgs
30 ) -> core::result::Result<(), ScenarioError>;
31}
32
33#[derive(Default)]
34pub(crate) struct ScenariosCmd {
35 scenarios: Vec<ScenarioCmd>
36}
37
38impl ScenariosCmd {
39 pub fn add_scenario<S: ScenarioMetadata + Scenario>(&mut self, scenario: S) {
40 self.scenarios.push(ScenarioCmd::new(scenario));
41 }
42}
43
44impl OdraCommand for ScenariosCmd {
45 fn run(
46 &self,
47 env: &HostEnv,
48 args: &ArgMatches,
49 types: &CustomTypeSet,
50 container: &DeployedContractsContainer
51 ) -> Result<()> {
52 args.subcommand()
53 .map(|(scenario_name, scenario_args)| {
54 self.scenarios
55 .iter()
56 .find(|cmd| cmd.name == scenario_name)
57 .map(|scenario| scenario.run(env, scenario_args, types, container))
58 .unwrap_or(Err(anyhow::anyhow!("No scenario found")))
59 })
60 .unwrap_or(Err(anyhow::anyhow!("No scenario found")))
61 }
62}
63
64impl From<&ScenariosCmd> for Command {
65 fn from(value: &ScenariosCmd) -> Self {
66 Command::new(SCENARIOS_SUBCOMMAND)
67 .about("Commands for interacting with scenarios")
68 .subcommand_required(true)
69 .arg_required_else_help(true)
70 .subcommands(&value.scenarios)
71 }
72}
73
74struct ScenarioCmd {
79 name: String,
80 description: String,
81 scenario: Box<dyn Scenario>
82}
83
84impl ScenarioCmd {
85 pub fn new<S: ScenarioMetadata + Scenario>(scenario: S) -> Self {
86 ScenarioCmd {
87 name: S::NAME.to_string(),
88 description: S::DESCRIPTION.to_string(),
89 scenario: Box::new(scenario)
90 }
91 }
92}
93
94impl OdraCommand for ScenarioCmd {
95 fn run(
96 &self,
97 env: &HostEnv,
98 args: &ArgMatches,
99 _types: &CustomTypeSet,
100 container: &DeployedContractsContainer
101 ) -> Result<()> {
102 let args = ScenarioArgs::new(args);
103 env.set_captures_events(false);
104 self.scenario.run(env, container, args)?;
105 Ok(())
106 }
107}
108
109impl From<&ScenarioCmd> for Command {
110 fn from(value: &ScenarioCmd) -> Self {
111 Command::new(&value.name)
112 .about(&value.description)
113 .args(value.scenario.args())
114 }
115}
116
117#[derive(Debug, Error)]
119pub enum ScenarioError {
120 #[error("Odra error: {message}")]
121 OdraError { message: String },
122 #[error("Contract read error: {0}")]
123 ContractReadError(#[from] ContractError),
124 #[error("Arg error")]
125 ArgError(#[from] ArgError),
126 #[error("Types error")]
127 TypesError(#[from] types::Error),
128 #[error("Missing scenario argument: {0}")]
129 MissingScenarioArg(String)
130}
131
132impl From<OdraError> for ScenarioError {
133 fn from(err: OdraError) -> Self {
134 ScenarioError::OdraError {
135 message: format!("{:?}", err)
136 }
137 }
138}
139
140pub struct ScenarioArgs<'a>(&'a ArgMatches);
142
143impl<'a> ScenarioArgs<'a> {
144 pub(crate) fn new(matches: &'a ArgMatches) -> Self {
145 Self(matches)
146 }
147
148 pub fn get_single<T: NamedCLTyped + FromBytes + CLTyped>(
149 &self,
150 name: &str
151 ) -> Result<T, ScenarioError> {
152 let arg = self
153 .0
154 .try_get_one::<CLValue>(name)
155 .map_err(|_| ScenarioError::ArgError(ArgError::UnexpectedArg(name.to_string())))?
156 .ok_or(ArgError::MissingArg(name.to_string()))?;
157
158 arg.clone()
159 .into_t::<T>()
160 .map_err(|_| ScenarioError::ArgError(ArgError::Deserialization))
161 }
162
163 pub fn get_many<T: NamedCLTyped + FromBytes + CLTyped>(
164 &self,
165 name: &str
166 ) -> Result<Vec<T>, ScenarioError> {
167 self.0
168 .try_get_many::<CLValue>(name)
169 .map_err(|_| ScenarioError::ArgError(ArgError::UnexpectedArg(name.to_string())))?
170 .unwrap_or_default()
171 .collect::<Vec<_>>()
172 .into_iter()
173 .map(|value| value.clone().into_t::<T>())
174 .collect::<Result<Vec<T>, _>>()
175 .map_err(|_| ScenarioError::ArgError(ArgError::Deserialization))
176 }
177}
178
179#[derive(Debug, Error, PartialEq)]
181pub enum ArgError {
182 #[error("Arg deserialization failed")]
183 Deserialization,
184 #[error("Multiple values expected")]
185 ManyExpected,
186 #[error("Single value expected")]
187 SingleExpected,
188 #[error("Missing arg: {0}")]
189 MissingArg(String),
190 #[error("Unexpected arg: {0}")]
191 UnexpectedArg(String)
192}
193
194pub trait ScenarioMetadata {
196 const NAME: &'static str;
197 const DESCRIPTION: &'static str;
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::test_utils;
204 use odra::schema::casper_contract_schema::NamedCLType;
205
206 struct TestScenario;
207
208 impl Scenario for TestScenario {
209 fn args(&self) -> Vec<CommandArg> {
210 vec![
211 CommandArg::new("arg1", "First argument", NamedCLType::U32).required(),
212 CommandArg::new("arg2", "Second argument", NamedCLType::String),
213 CommandArg::new("arg3", "Optional argument", NamedCLType::String).list(),
214 ]
215 }
216
217 fn run(
218 &self,
219 _env: &HostEnv,
220 _container: &DeployedContractsContainer,
221 args: ScenarioArgs
222 ) -> core::result::Result<(), ScenarioError> {
223 _ = args.get_single::<u32>("arg1")?;
224 _ = args.get_single::<String>("arg2")?;
225 _ = args.get_many::<String>("arg3")?;
226 Ok(())
227 }
228 }
229
230 impl ScenarioMetadata for TestScenario {
231 const NAME: &'static str = "test_scenario";
232 const DESCRIPTION: &'static str = "A test scenario for unit testing";
233 }
234
235 #[test]
236 fn test_scenarios_command() {
237 let mut scenarios = ScenariosCmd::default();
238 scenarios.add_scenario(TestScenario);
239
240 let cmd: Command = (&scenarios).into();
241
242 assert_eq!(cmd.get_name(), SCENARIOS_SUBCOMMAND);
243 assert!(cmd
244 .get_subcommands()
245 .any(|c| c.get_name() == "test_scenario"));
246 }
247
248 #[test]
249 fn test_match_required_args() {
250 let cmd = ScenarioCmd::new(TestScenario);
251 let cmd: Command = (&cmd).into();
252
253 let matches = cmd.try_get_matches_from(vec!["odra-cli", "--arg1", "1"]);
254 assert!(matches.is_ok());
255 assert_eq!(
256 matches.unwrap().get_one::<CLValue>("arg1"),
257 Some(&CLValue::from_t(1u32).unwrap())
258 );
259 }
260
261 #[test]
262 fn test_match_required_arg_missing() {
263 let cmd = ScenarioCmd::new(TestScenario);
264 let cmd: Command = (&cmd).into();
265
266 let matches = cmd.try_get_matches_from(vec!["odra-cli"]);
267 assert!(matches.is_err());
268 assert_eq!(
269 matches.unwrap_err().kind(),
270 clap::error::ErrorKind::MissingRequiredArgument
271 );
272 }
273
274 #[test]
275 fn test_match_required_arg_with_wrong_type() {
276 let cmd = ScenarioCmd::new(TestScenario);
277 let cmd: Command = (&cmd).into();
278
279 let matches = cmd.try_get_matches_from(vec!["odra-cli", "--arg1", "not_a_number"]);
280 assert!(matches.is_err());
281 assert_eq!(
282 matches.unwrap_err().kind(),
283 clap::error::ErrorKind::InvalidValue
284 );
285 }
286
287 #[test]
288 fn test_match_optional_args() {
289 let cmd = ScenarioCmd::new(TestScenario);
290 let cmd: Command = (&cmd).into();
291
292 let matches = cmd.try_get_matches_from(vec![
293 "odra-cli", "--arg1", "1", "--arg2", "test", "--arg3", "value1", "--arg3", "value2",
294 ]);
295 assert!(matches.is_ok());
296 let matches = matches.unwrap();
297 assert_eq!(
298 matches.get_one::<CLValue>("arg1"),
299 Some(&CLValue::from_t(1u32).unwrap())
300 );
301 assert_eq!(
302 matches.get_one::<CLValue>("arg2"),
303 Some(&CLValue::from_t("test".to_string()).unwrap())
304 );
305 assert_eq!(
306 matches
307 .get_many::<CLValue>("arg3")
308 .unwrap()
309 .collect::<Vec<_>>(),
310 vec![
311 &CLValue::from_t("value1".to_string()).unwrap(),
312 &CLValue::from_t("value2".to_string()).unwrap()
313 ]
314 );
315 }
316
317 #[test]
318 fn test_matching_list_args() {
319 let scenario = ScenarioCmd::new(TestScenario);
320 let cmd: Command = (&scenario).into();
321
322 let matches = cmd.try_get_matches_from(vec![
323 "odra-cli", "--arg1", "1", "--arg3", "value1", "--arg3", "value2",
324 ]);
325 assert!(matches.is_ok());
326 let matches = matches.unwrap();
327 assert_eq!(
328 matches
329 .get_many::<CLValue>("arg3")
330 .unwrap()
331 .collect::<Vec<_>>(),
332 vec![
333 &CLValue::from_t("value1".to_string()).unwrap(),
334 &CLValue::from_t("value2".to_string()).unwrap()
335 ]
336 );
337
338 let cmd: Command = (&scenario).into();
340 let matches = cmd.try_get_matches_from(vec![
341 "odra-cli", "--arg1", "1", "--arg2", "value1", "--arg2", "value2",
342 ]);
343
344 assert!(matches.is_err());
345 assert_eq!(
346 matches.unwrap_err().kind(),
347 clap::error::ErrorKind::ArgumentConflict
348 );
349 }
350
351 #[test]
352 fn test_scenario_args_get_single() {
353 let scenario = ScenarioCmd::new(TestScenario);
354 let cmd: Command = (&scenario).into();
355
356 let matches = cmd
357 .try_get_matches_from(vec!["odra-cli", "--arg1", "42", "--arg2", "test_value"])
358 .unwrap();
359
360 let args = ScenarioArgs::new(&matches);
361 let arg1: u32 = args.get_single("arg1").unwrap();
362 assert_eq!(arg1, 42);
363
364 let arg2: String = args.get_single("arg2").unwrap();
365 assert_eq!(arg2, "test_value");
366 }
367
368 #[test]
369 fn test_scenario_args_get_many() {
370 let scenario = ScenarioCmd::new(TestScenario);
371 let cmd: Command = (&scenario).into();
372
373 let matches = cmd
374 .try_get_matches_from(vec![
375 "odra-cli", "--arg1", "42", "--arg3", "value1", "--arg3", "value2",
376 ])
377 .unwrap();
378
379 let args = ScenarioArgs::new(&matches);
380 let arg3: Vec<String> = args.get_many("arg3").unwrap();
381 assert_eq!(arg3, vec!["value1".to_string(), "value2".to_string()]);
382 }
383
384 #[test]
385 fn test_run_cmd() {
386 let scenario = ScenarioCmd::new(TestScenario);
387 let cmd: Command = (&scenario).into();
388
389 let matches = cmd
390 .try_get_matches_from(vec![
391 "odra-cli",
392 "--arg1",
393 "42",
394 "--arg2",
395 "test_value",
396 "--arg3",
397 "value1",
398 "--arg3",
399 "value2",
400 ])
401 .unwrap();
402
403 let env = test_utils::mock_host_env();
404 let container = test_utils::mock_contracts_container();
405 let result = scenario.run(&env, &matches, &CustomTypeSet::default(), &container);
406 assert!(result.is_ok());
407 }
408
409 #[test]
410 fn test_scenario_args_missing_arg() {
411 let scenario = ScenarioCmd::new(TestScenario);
412 let cmd: Command = (&scenario).into();
413
414 let matches = cmd.get_matches_from(vec!["odra-cli", "--arg1", "1", "--arg2", "value1"]);
415 ScenarioArgs(&matches)
416 .get_single::<u32>("arg3")
417 .expect_err("Expected an error for missing arg3");
418
419 assert_eq!(ScenarioArgs(&matches).get_single::<u32>("arg1").unwrap(), 1);
420 }
421}