reifydb_testing/testscript/
command.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4// This file includes and modifies code from the toydb project (https://github.com/erikgrinaker/toydb),
5// originally licensed under the Apache License, Version 2.0.
6// Original copyright:
7//   Copyright (c) 2024 Erik Grinaker
8//
9// The original Apache License can be found at:
10//   http://www.apache.org/licenses/LICENSE-2.0
11
12use std::{
13	collections::{BTreeSet, HashSet, VecDeque},
14	error::Error,
15};
16
17/// A block, consisting of multiple commands.
18#[derive(Clone, Debug, PartialEq)]
19#[non_exhaustive]
20pub(crate) struct Block {
21	/// The commands in the block.
22	pub commands: Vec<Command>,
23	/// The literal string of the input commands. Used to generate the
24	/// output.
25	pub literal: String,
26	/// The block's line number position in the script.
27	pub line_number: u32,
28}
29
30/// A command.
31#[derive(Clone, PartialEq)]
32#[non_exhaustive]
33pub struct Command {
34	/// The name of the command. Never empty.
35	pub name: String,
36	/// The command's arguments, in the given order.
37	pub args: Vec<Argument>,
38	/// The command prefix, if given.
39	pub prefix: Option<String>,
40	/// Any command tags, if given.
41	pub tags: HashSet<String>,
42	/// Silences the output of this command. This is handled automatically,
43	/// the [`Runner`](crate::Runner) does not have to take this into
44	/// account.
45	pub silent: bool,
46	/// If true, the command is expected to fail with a panic or error. If
47	/// the command does not fail, the test fails.
48	pub fail: bool,
49	/// The command's line number position in the script.
50	pub line_number: u32,
51}
52
53impl std::fmt::Debug for Command {
54	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55		f.debug_struct("Command")
56            .field("name", &self.name)
57            .field("args", &self.args)
58            .field("prefix", &self.prefix)
59            // Use a sorted BTreeSet for test determinism.
60            .field("tags", &BTreeSet::from_iter(&self.tags))
61            .field("silent", &self.silent)
62            .field("fail", &self.fail)
63            .field("line_number", &self.line_number)
64            .finish()
65	}
66}
67
68impl Command {
69	/// Returns an argument consumer, for more convenient argument
70	/// processing. Does not affect [`Command::args`].
71	///
72	/// See the [module documentation](crate#argument-processing) for usage
73	/// examples.
74	pub fn consume_args(&self) -> ArgumentConsumer<'_> {
75		ArgumentConsumer::new(&self.args)
76	}
77}
78
79/// A command argument.
80#[derive(Clone, Debug, PartialEq)]
81#[non_exhaustive]
82pub struct Argument {
83	/// The argument key, for `key=value` style arguments. Not guaranteed
84	/// to be unique, the [`Runner`](crate::Runner) can handle this as
85	/// desired.
86	pub key: Option<String>,
87	/// The argument value. Can be empty.
88	pub value: String,
89}
90
91impl Argument {
92	/// Returns a name for the argument -- either the key, if given, or
93	/// value.
94	pub fn name(&self) -> &str {
95		match self.key.as_deref() {
96			Some(key) => key,
97			None => &self.value,
98		}
99	}
100
101	/// Parses the argument value as a T using flow::str::parse().
102	/// Convenience method that returns an improved error message as a
103	/// boxed error to ease error handling in a [`Runner`](crate::Runner).
104	pub fn parse<T>(&self) -> Result<T, Box<dyn Error>>
105	where
106		T: std::str::FromStr,
107		<T as std::str::FromStr>::Err: std::fmt::Display,
108	{
109		self.value.parse().map_err(|e| format!("invalid argument '{}': {e}", self.value).into())
110	}
111}
112
113/// Helper for argument processing, by returning and removing arguments on
114/// demand.
115///
116/// Created by [`Command::consume_args()`]. Implements [`Iterator`], but is also
117/// intended for out-of-order processing, unlike most iterators.
118pub struct ArgumentConsumer<'a> {
119	args: VecDeque<&'a Argument>,
120}
121
122impl<'a> Iterator for ArgumentConsumer<'a> {
123	type Item = &'a Argument;
124
125	/// Returns and removes the next argument, if any.
126	fn next(&mut self) -> Option<Self::Item> {
127		self.args.pop_front()
128	}
129}
130
131impl<'a> ArgumentConsumer<'a> {
132	/// Creates a new argument consumer.
133	fn new(args: &'a [Argument]) -> Self {
134		Self {
135			args: VecDeque::from_iter(args.iter()),
136		}
137	}
138
139	/// Looks up and removes a key/value argument by key. If multiple
140	/// arguments use the same key, the last one is returned (but all are
141	/// removed).
142	pub fn lookup(&mut self, key: &str) -> Option<&'a Argument> {
143		let arg = self.args.iter().rev().find(|a| a.key.as_deref() == Some(key)).copied();
144		if arg.is_some() {
145			self.args.retain(|a| a.key.as_deref() != Some(key))
146		}
147		arg
148	}
149
150	/// Looks up and parses a key/value argument by key, removing it. If
151	/// parsing errors, the argument is not removed.
152	pub fn lookup_parse<T>(&mut self, key: &str) -> Result<Option<T>, Box<dyn Error>>
153	where
154		T: std::str::FromStr,
155		<T as std::str::FromStr>::Err: std::fmt::Display,
156	{
157		let value = self
158			.args
159			.iter()
160			.rev()
161			.find(|a| a.key.as_deref() == Some(key))
162			.map(|a| a.parse())
163			.transpose()?;
164		if value.is_some() {
165			self.args.retain(|a| a.key.as_deref() != Some(key))
166		}
167		Ok(value)
168	}
169
170	/// Returns and removes the next key/value argument, if any.
171	pub fn next_key(&mut self) -> Option<&'a Argument> {
172		self.args.iter().position(|a| a.key.is_some()).map(|i| self.args.remove(i).unwrap())
173	}
174
175	/// Returns and removes the next positional argument, if any.
176	pub fn next_pos(&mut self) -> Option<&'a Argument> {
177		self.args.iter().position(|a| a.key.is_none()).map(|i| self.args.remove(i).unwrap())
178	}
179
180	/// Rejects any remaining arguments with an error.
181	pub fn reject_rest(&self) -> Result<(), Box<dyn Error>> {
182		if let Some(arg) = self.args.front() {
183			return Err(format!("invalid argument '{}'", arg.name()).into());
184		}
185		Ok(())
186	}
187
188	/// Returns and removes all remaining arguments.
189	pub fn rest(&mut self) -> Vec<&'a Argument> {
190		self.args.drain(..).collect()
191	}
192
193	/// Returns and removes all remaining key/value arguments.
194	pub fn rest_key(&mut self) -> Vec<&'a Argument> {
195		let keyed: Vec<_> = self.args.iter().filter(|a| a.key.is_some()).copied().collect();
196		if !keyed.is_empty() {
197			self.args.retain(|a| a.key.is_none());
198		}
199		keyed
200	}
201
202	/// Returns and removes all remaining positional arguments.
203	pub fn rest_pos(&mut self) -> Vec<&'a Argument> {
204		let pos: Vec<_> = self.args.iter().filter(|a| a.key.is_none()).copied().collect();
205		if !pos.is_empty() {
206			self.args.retain(|a| a.key.is_some());
207		}
208		pos
209	}
210}
211
212#[cfg(test)]
213mod tests {
214	use super::*;
215
216	/// Constructs an Argument from a string value or key => value.
217	macro_rules! arg {
218		($value:expr) => {
219			Argument {
220				key: None,
221				value: $value.to_string(),
222			}
223		};
224		($key:expr => $value:expr) => {
225			Argument {
226				key: Some($key.to_string()),
227				value: $value.to_string(),
228			}
229		};
230	}
231
232	/// Constructs a Command by parsing the given input string.
233	macro_rules! cmd {
234		($input:expr) => {{ crate::testscript::parser::parse_command(&format!("{}\n", $input)).expect("invalid command") }};
235	}
236
237	/// Tests Argument.name().
238	#[test]
239	fn test_argument_name() {
240		assert_eq!(arg!("value").name(), "value");
241		assert_eq!(arg!("key" => "value").name(), "key");
242	}
243
244	/// Basic tests of Argument.parse(). Not comprehensive, since it
245	/// dispatches to flow::str::parse().
246	#[test]
247	fn test_argument_parse() {
248		assert_eq!(arg!("-1").parse::<i64>().unwrap(), -1_i64);
249		assert_eq!(arg!("0").parse::<i64>().unwrap(), 0_i64);
250		assert_eq!(arg!("1").parse::<i64>().unwrap(), 1_i64);
251
252		assert_eq!(
253			arg!("").parse::<i64>().unwrap_err().to_string(),
254			"invalid argument '': cannot parse integer from empty string"
255		);
256		assert_eq!(
257			arg!("foo").parse::<i64>().unwrap_err().to_string(),
258			"invalid argument 'foo': invalid digit found in string"
259		);
260
261		assert!(!arg!("false").parse::<bool>().unwrap());
262		assert!(arg!("true").parse::<bool>().unwrap());
263
264		assert_eq!(
265			arg!("").parse::<bool>().unwrap_err().to_string(),
266			"invalid argument '': provided string was not `true` or `false`"
267		);
268	}
269
270	/// Tests Command.consume_args(). ArgumentConsumer is tested separately.
271	#[test]
272	fn test_command_consume_args() {
273		let cmd = cmd!("cmd foo key=value bar");
274		assert_eq!(cmd.consume_args().rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2]]);
275	}
276
277	/// Tests ArgumentConsumer.lookup().
278	#[test]
279	fn test_argument_consumer_lookup() {
280		let cmd = cmd!("cmd value key=value foo=bar key=other");
281
282		// lookup() returns None on unknown keys, including ones that
283		// match a value argument.
284		let mut args = cmd.consume_args();
285		assert_eq!(args.lookup("unknown"), None);
286		assert_eq!(args.lookup("value"), None);
287		assert_eq!(args.rest().len(), 4);
288
289		// lookup() removes duplicate keys, returning the last.
290		let mut args = cmd.consume_args();
291		assert_eq!(args.lookup("key"), Some(&cmd.args[3]));
292		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
293
294		// lookup() removes single keys.
295		let mut args = cmd.consume_args();
296		assert_eq!(args.lookup("foo"), Some(&cmd.args[2]));
297		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
298	}
299
300	/// Tests ArgumentConsumer.lookup_parse().
301	#[test]
302	fn test_argument_consumer_lookup_parse() {
303		let cmd = cmd!("cmd value key=1 foo=bar key=2");
304
305		// lookup_parse() returns None on unknown keys, including ones
306		// that match a value argument.
307		let mut args = cmd.consume_args();
308		assert_eq!(args.lookup_parse::<String>("unknown").unwrap(), None);
309		assert_eq!(args.lookup_parse::<String>("value").unwrap(), None);
310		assert_eq!(args.rest().len(), 4);
311
312		// lookup_parse() parses and removes duplicate keys, returning
313		// the last.
314		let mut args = cmd.consume_args();
315		assert_eq!(args.lookup_parse("key").unwrap(), Some(2));
316		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
317
318		// lookup_parse() parses and removes single keys, with string
319		// parsing being a noop.
320		let mut args = cmd.consume_args();
321		assert_eq!(args.lookup_parse("foo").unwrap(), Some("bar".to_string()));
322		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
323
324		// lookup_parse() does not remove arguments on parse errors,
325		// even with duplicate keys.
326		let mut args = cmd.consume_args();
327		assert!(args.lookup_parse::<bool>("key").is_err());
328		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
329	}
330
331	/// Tests ArgumentConsumer.next(), next_pos(), and next_key().
332	#[test]
333	fn test_argument_consumer_next() {
334		let cmd = cmd!("cmd foo key=1 key=2 bar");
335
336		// next() returns references to all arguments and consumes them.
337		let mut args = cmd.consume_args();
338		assert_eq!(args.next(), Some(&cmd.args[0]));
339		assert_eq!(args.next(), Some(&cmd.args[1]));
340		assert_eq!(args.next(), Some(&cmd.args[2]));
341		assert_eq!(args.next(), Some(&cmd.args[3]));
342		assert_eq!(args.next(), None);
343		assert!(args.rest().is_empty());
344
345		// next_key() returns references to key/value arguments and
346		// consumes them.
347		let mut args = cmd.consume_args();
348		assert_eq!(args.next_key(), Some(&cmd.args[1]));
349		assert_eq!(args.next_key(), Some(&cmd.args[2]));
350		assert_eq!(args.next_key(), None);
351		assert_eq!(args.next(), Some(&cmd.args[0]));
352		assert_eq!(args.next(), Some(&cmd.args[3]));
353		assert_eq!(args.next(), None);
354		assert!(args.rest().is_empty());
355
356		// next_pos() returns references to key/value arguments and
357		// consumes them.
358		let mut args = cmd.consume_args();
359		assert_eq!(args.next_pos(), Some(&cmd.args[0]));
360		assert_eq!(args.next_pos(), Some(&cmd.args[3]));
361		assert_eq!(args.next_pos(), None);
362		assert_eq!(args.next(), Some(&cmd.args[1]));
363		assert_eq!(args.next(), Some(&cmd.args[2]));
364		assert_eq!(args.next(), None);
365		assert!(args.rest().is_empty());
366	}
367
368	/// Tests ArgumentConsumer.reject_rest().
369	#[test]
370	fn test_argument_consumer_reject_rest() {
371		// Empty args return Ok.
372		let cmd = cmd!("cmd");
373		assert!(cmd.consume_args().reject_rest().is_ok());
374
375		// Positional argument fails. It does not consume the arg.
376		let cmd = cmd!("cmd value");
377		let mut args = cmd.consume_args();
378		assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'value'");
379		assert!(!args.rest().is_empty());
380
381		// Key/value argument fails.
382		let cmd = cmd!("cmd key=value");
383		let mut args = cmd.consume_args();
384		assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'key'");
385		assert!(!args.rest().is_empty());
386	}
387
388	/// Tests ArgumentConsumer.rest(), rest_pos() and rest_key().
389	#[test]
390	fn test_argument_consumer_rest() {
391		let cmd = cmd!("cmd foo key=1 key=2 bar");
392
393		// rest() returns references to all arguments and consumes them.
394		let mut args = cmd.consume_args();
395		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
396		assert!(args.rest().is_empty());
397
398		// rest_pos() returns and consumes positional arguments.
399		let mut args = cmd.consume_args();
400		assert_eq!(args.rest_pos(), vec![&cmd.args[0], &cmd.args[3]]);
401		assert!(args.rest_pos().is_empty());
402		assert_eq!(args.rest(), vec![&cmd.args[1], &cmd.args[2]]);
403
404		// rest_key() returns and consumes key/value arguments.
405		let mut args = cmd.consume_args();
406		assert_eq!(args.rest_key(), vec![&cmd.args[1], &cmd.args[2]]);
407		assert!(args.rest_key().is_empty());
408		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[3]]);
409	}
410}