Skip to main content

reifydb_testing/testscript/
command.rs

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