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