diff --git a/Cargo.toml b/Cargo.toml index 4dda779..865b246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,14 @@ homepage = "https://github.com/killercup/assert_cli" documentation = "http://killercup.github.io/assert_cli/" readme = "README.md" categories = ["development-tools::testing"] -keywords = ["cli", "testing"] +keywords = ["cli", "testing", "assert"] build = "build.rs" [dependencies] colored = "1.4" difference = "1.0" error-chain = "0.10.0" +rustc-serialize = "0.3" [build-dependencies] skeptic = "0.5" diff --git a/README.md b/README.md index 76cef87..ae97974 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # Assert CLI -> Test CLI Applications. +> **Test CLI Applications** - This crate checks the output of a child process is as expected. -Currently, this crate only includes basic functionality to check the output of a child process -is as expected. - -[![Build Status](https://travis-ci.org/killercup/assert_cli.svg)](https://travis-ci.org/killercup/assert_cli) [![Coverage Status](https://coveralls.io/repos/killercup/assert_cli/badge.svg?branch=master&service=github)](https://coveralls.io/github/killercup/assert_cli?branch=master) - -**[Documentation](http://killercup.github.io/assert_cli/)** +[![Build Status](https://travis-ci.org/killercup/assert_cli.svg)](https://travis-ci.org/killercup/assert_cli) [![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] ## Install @@ -30,13 +25,13 @@ fn main() { } ``` -Or if you'd rather use the macro: +Or if you'd rather use the macro, to save you some writing: ```rust #[macro_use] extern crate assert_cli; fn main() { - assert_cmd!(echo 42).succeeds().and().prints("42").unwrap(); + assert_cmd!(echo "42").prints("42").unwrap(); } ``` @@ -47,33 +42,42 @@ And here is one that will fail (which also shows `execute` which returns a #[macro_use] extern crate assert_cli; fn main() { - let test = assert_cmd!(grep amet Cargo.toml) - .fails_with(1) + let test = assert_cmd!(ls "foo-bar-foo") + .fails() + .and() + .prints_error("foo-bar-foo") .execute(); - assert!(test.is_err()); + assert!(test.is_ok()); } ``` -If you want to check for the program's output, you can use `print` or -`print_exactly`: +If you want to match the program's output _exactly_, you can use +`prints_exactly`: -```rust,should_panic="Assert CLI failure" +```rust,should_panic #[macro_use] extern crate assert_cli; fn main() { - assert_cmd!("wc" "README.md") + assert_cmd!(wc "README.md") .prints_exactly("1337 README.md") .unwrap(); } ``` -this will show a nice, colorful diff in your terminal, like this: +... which has the benefit to show a nice, colorful diff in your terminal, +like this: ```diff -1337 +92 ``` +**Tip**: Enclose arguments in the `assert_cmd!` macro in quotes `"`, + if there are special characters, which the macro doesn't accept, e.g. + `assert_cmd!(cat "foo.txt")`. + +More detailed information is available in the [documentation]. :-) + ## License Licensed under either of @@ -89,3 +93,5 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[Documentation]: http://killercup.github.io/assert_cli/ diff --git a/src/errors.rs b/src/errors.rs index b2751f5..e4b7ae8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +const ERROR_PREFIX: &'static str = "CLI assertion failed"; + error_chain! { foreign_links { Io(::std::io::Error); @@ -7,42 +9,43 @@ error_chain! { StatusMismatch(cmd: Vec, expected: bool) { description("Wrong status") display( - "Command {:?} {got} but expected it to {expected}", + "{}: `(command `{}` expected to {})` (command {})", + ERROR_PREFIX, cmd.join(" "), - got = if *expected { "failed" } else { "succeed" }, - expected = if *expected { "succeed" } else { "failed" }, + expected = if *expected { "succeed" } else { "fail" }, + got = if *expected { "failed" } else { "succeeded" }, ) } ExitCodeMismatch(cmd: Vec, expected: Option, got: Option) { description("Wrong exit code") display( - "Command {:?} exited with code {:?} but expected it to be {:?}", - cmd.join(" "), got, expected, + "{}: `(exit code of `{}` expected to be `{:?}`)` (exit code was: `{:?}`)", + ERROR_PREFIX, + cmd.join(" "), + expected, + got, ) } - OutputMismatch(expected: String, got: String) { + OutputMismatch(output_name: String, cmd: Vec, expected: String, got: String) { description("Output was not as expected") display( - "Expected output to contain\n{}\nbut could not find it in\n{}", + "{}: `({} of `{}` expected to contain `{:?}`)` (output was: `{:?}`)", + ERROR_PREFIX, + output_name, + cmd.join(" "), expected, got, ) } - ExactOutputMismatch(diff: String) { + ExactOutputMismatch(output_name: String, cmd: Vec, diff: String) { description("Output was not as expected") - display("{}", diff) - } - ErrorOutputMismatch(expected: String, got: String) { - description("Stderr output was not as expected") display( - "Expected stderr output to contain\n{}\nbut could not find it in\n{}", - expected, - got, + "{}: `({} of `{}` was not as expected)`\n{}\n", + ERROR_PREFIX, + output_name, + cmd.join(" "), + diff.trim() ) } - ExactErrorOutputMismatch(diff: String) { - description("Stderr output was not as expected") - display("{}", diff) - } } } diff --git a/src/lib.rs b/src/lib.rs index b319cca..80e28bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,17 +2,23 @@ //! //! This crate's goal is to provide you some very easy tools to test your CLI //! applications. It can currently execute child processes and validate their -//! exit status as well as stdout output against your assertions. +//! exit status as well as stdout and stderr output against your assertions. //! -//! ## Examples +//! Include the crate like //! -//! Here's a trivial example: +//! ```rust +//! #[macro_use] // <-- import the convenience macro (optional) +//! extern crate assert_cli; +//! # fn main() { } +//! ``` //! -//! ```rust extern crate assert_cli; +//! ## Basic Examples +//! +//! Here's a trivial example: //! +//! ```rust //! assert_cli::Assert::command(&["echo", "42"]) -//! .succeeds() -//! .and().prints("42") +//! .prints("42") //! .unwrap(); //! ``` //! @@ -20,7 +26,7 @@ //! //! ```rust,should_panic //! assert_cli::Assert::command(&["echo", "42"]) -//! .prints("1337") +//! .prints_exactly("1337") //! .unwrap(); //! ``` //! @@ -31,31 +37,67 @@ //! +42 //! ``` //! -//! If you are testing a Rust binary crate, you can start with -//! `Assert::main_binary()` to use `cargo run` as command. Or, if you want to -//! run a specific binary (if you have more than one), use -//! `Assert::cargo_binary`. +//! ## `assert_cmd!` Macro //! -//! Alternatively, you can use the `assert_cmd!` macro to construct the command: +//! Alternatively, you can use the `assert_cmd!` macro to construct the command more conveniently, +//! but please carefully read the limitations below, or this may seriously go wrong. //! //! ```rust -//! #[macro_use] extern crate assert_cli; +//! # #[macro_use] extern crate assert_cli; +//! # fn main() { +//! assert_cmd!(echo "42").prints("42").unwrap(); +//! # } +//! ``` +//! +//! **Tips** +//! +//! - Don't forget to import the crate with `#[macro_use]`. ;-) +//! - Enclose arguments in the `assert_cmd!` macro in quotes `"`, +//! if there are special characters, which the macro doesn't accept, e.g. +//! `assert_cmd!(cat "foo.txt")`. //! +//! ## Exit Status +//! +//! All assertion default to checking that the command exited with success. +//! +//! However, when you expect a command to fail, you can express it like this: +//! +//! ```rust +//! # #[macro_use] extern crate assert_cli; //! # fn main() { -//! assert_cmd!(echo 42).succeeds().prints("42").unwrap(); +//! assert_cmd!(cat "non-existing-file") +//! .fails() +//! .and() +//! .prints_error("non-existing-file") +//! .unwrap(); //! # } //! ``` //! -//! (Make sure to include the crate as `#[macro_use] extern crate assert_cli;`!) +//! Some notes on this: +//! +//! - Use `fails_with` to assert a specific exit status. +//! - There is also a `succeeds` method, but this is already the implicit default +//! and can usually be omitted. +//! - We can inspect the output of **stderr** with `prints_error` and `prints_error_exactly`. +//! - The `and` method has no effect, other than to make everything more readable. +//! Feel free to use it. :-) +//! +//! ## Assert CLI Crates +//! +//! If you are testing a Rust binary crate, you can start with +//! `Assert::main_binary()` to use `cargo run` as command. Or, if you want to +//! run a specific binary (if you have more than one), use +//! `Assert::cargo_binary`. +//! +//! ## Don't Panic! //! //! If you don't want it to panic when the assertions are not met, simply call //! `.execute` instead of `.unwrap` to get a `Result`: //! //! ```rust -//! #[macro_use] extern crate assert_cli; -//! +//! # #[macro_use] extern crate assert_cli; //! # fn main() { -//! let x = assert_cmd!(echo 1337).prints_exactly("42").execute(); +//! let x = assert_cmd!(echo "1337").prints_exactly("42").execute(); //! assert!(x.is_err()); //! # } //! ``` @@ -64,51 +106,88 @@ extern crate difference; #[macro_use] extern crate error_chain; +extern crate rustc_serialize; -use std::process::Command; +use std::process::{Command, Output}; +use std::fmt; use difference::Changeset; mod errors; use errors::*; +#[macro_use] mod macros; +pub use macros::flatten_escaped_string; + mod diff; -/// Assertions for a specific command +/// Assertions for a specific command. #[derive(Debug)] pub struct Assert { cmd: Vec, - expect_success: bool, + expect_success: Option, expect_exit_code: Option, - expect_output: Option, - fuzzy_output: bool, - expect_error_output: Option, - fuzzy_error_output: bool, + expect_stdout: Option, + expect_stderr: Option, +} + +#[derive(Debug)] +struct OutputAssertion { + expect: String, + fuzzy: bool, +} + +#[derive(Debug, Copy, Clone)] +enum OutputType { + StdOut, + StdErr, +} + +impl OutputType { + fn select<'a>(&self, o: &'a Output) -> &'a [u8] { + match *self { + OutputType::StdOut => &o.stdout, + OutputType::StdErr => &o.stderr, + } + } +} + +impl fmt::Display for OutputType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + OutputType::StdOut => write!(f, "stdout"), + OutputType::StdErr => write!(f, "stderr"), + } + } } impl std::default::Default for Assert { /// Construct an assert using `cargo run --` as command. + /// + /// Defaults to asserting _successful_ execution. fn default() -> Self { Assert { cmd: vec!["cargo", "run", "--"] .into_iter().map(String::from).collect(), - expect_success: true, + expect_success: Some(true), expect_exit_code: None, - expect_output: None, - fuzzy_output: false, - expect_error_output: None, - fuzzy_error_output: false, + expect_stdout: None, + expect_stderr: None, } } } impl Assert { - /// Use the crate's main binary as command + /// Run the crate's main binary. + /// + /// Defaults to asserting _successful_ execution. pub fn main_binary() -> Self { Assert::default() } - /// Use the crate's main binary as command + /// Run a specific binary of the current crate. + /// + /// Defaults to asserting _successful_ execution. pub fn cargo_binary(name: &str) -> Self { Assert { cmd: vec!["cargo", "run", "--bin", name, "--"] @@ -117,7 +196,9 @@ impl Assert { } } - /// Use custom command + /// Run a custom command. + /// + /// Defaults to asserting _successful_ execution. /// /// # Examples /// @@ -125,7 +206,6 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "1337"]) - /// .succeeds() /// .unwrap(); /// ``` pub fn command(cmd: &[&str]) -> Self { @@ -135,7 +215,7 @@ impl Assert { } } - /// Add arguments to the command + /// Add arguments to the command. /// /// # Examples /// @@ -144,7 +224,6 @@ impl Assert { /// /// assert_cli::Assert::command(&["echo"]) /// .with_args(&["42"]) - /// .succeeds() /// .prints("42") /// .unwrap(); /// ``` @@ -153,7 +232,7 @@ impl Assert { self } - /// Small helper to make chains more readable + /// Small helper to make chains more readable. /// /// # Examples /// @@ -161,14 +240,14 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .succeeds().and().prints("42") + /// .prints("42") /// .unwrap(); /// ``` pub fn and(self) -> Self { self } - /// Expect the command to be executed successfully + /// Expect the command to be executed successfully. /// /// # Examples /// @@ -176,48 +255,55 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .succeeds() /// .unwrap(); /// ``` pub fn succeeds(mut self) -> Self { - self.expect_success = true; + self.expect_exit_code = None; + self.expect_success = Some(true); self } - /// Expect the command to fail + /// Expect the command to fail. + /// + /// Note: This does not include shell failures like `command not found`. I.e. the + /// command must _run_ and fail for this assertion to pass. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails() + /// .and() + /// .prints_error("non-existing-file") /// .unwrap(); /// ``` pub fn fails(mut self) -> Self { - self.expect_success = false; + self.expect_success = Some(false); self } - /// Expect the command to fail and return a specific error code + /// Expect the command to fail and return a specific error code. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails_with(1) + /// .and() + /// .prints_error_exactly("cat: non-existing-file: No such file or directory") /// .unwrap(); /// ``` pub fn fails_with(mut self, expect_exit_code: i32) -> Self { - self.expect_success = false; + self.expect_success = Some(false); self.expect_exit_code = Some(expect_exit_code); self } - /// Expect the command's output to contain `output` + /// Expect the command's output to **contain** `output`. /// /// # Examples /// @@ -229,12 +315,14 @@ impl Assert { /// .unwrap(); /// ``` pub fn prints>(mut self, output: O) -> Self { - self.expect_output = Some(output.into()); - self.fuzzy_output = true; + self.expect_stdout = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + }); self } - /// Expect the command to output exactly this `output` + /// Expect the command to output **exactly** this `output`. /// /// # Examples /// @@ -246,48 +334,56 @@ impl Assert { /// .unwrap(); /// ``` pub fn prints_exactly>(mut self, output: O) -> Self { - self.expect_output = Some(output.into()); - self.fuzzy_output = false; + self.expect_stdout = Some(OutputAssertion { + expect: output.into(), + fuzzy: false, + }); self } - /// Expect the command's stderr output to contain `output` + /// Expect the command's stderr output to **contain** `output`. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails() - /// .prints_error("non-exisiting-file") + /// .and() + /// .prints_error("non-existing-file") /// .unwrap(); /// ``` pub fn prints_error>(mut self, output: O) -> Self { - self.expect_error_output = Some(output.into()); - self.fuzzy_error_output = true; + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + }); self } - /// Expect the command to output exactly this `output` to stderr + /// Expect the command to output **exactly** this `output` to stderr. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) - /// .fails() - /// .prints_error_exactly("cat: non-exisiting-file: No such file or directory") + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) + /// .fails_with(1) + /// .and() + /// .prints_error_exactly("cat: non-existing-file: No such file or directory") /// .unwrap(); /// ``` pub fn prints_error_exactly>(mut self, output: O) -> Self { - self.expect_error_output = Some(output.into()); - self.fuzzy_error_output = false; + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: false, + }); self } - /// Execute the command and check the assertions + /// Execute the command and check the assertions. /// /// # Examples /// @@ -295,7 +391,7 @@ impl Assert { /// extern crate assert_cli; /// /// let test = assert_cli::Assert::command(&["echo", "42"]) - /// .succeeds() + /// .prints("42") /// .execute(); /// assert!(test.is_ok()); /// ``` @@ -307,11 +403,13 @@ impl Assert { let output = command.output()?; - if self.expect_success != output.status.success() { - bail!(ErrorKind::StatusMismatch( - self.cmd.clone(), - self.expect_success.clone(), - )); + if let Some(expect_success) = self.expect_success { + if expect_success != output.status.success() { + bail!(ErrorKind::StatusMismatch( + self.cmd.clone(), + expect_success, + )); + } } if self.expect_exit_code.is_some() && @@ -323,46 +421,55 @@ impl Assert { )); } - let stdout = String::from_utf8_lossy(&output.stdout); - match (self.expect_output, self.fuzzy_output) { - (Some(ref expected_output), true) if !stdout.contains(expected_output) => { + self.assert_output(OutputType::StdOut, &output)?; + self.assert_output(OutputType::StdErr, &output)?; + + Ok(()) + } + + /// Perform the appropriate output assertion. + fn assert_output(&self, output_type: OutputType, output: &Output) -> Result<()> { + let observed = String::from_utf8_lossy(output_type.select(output)); + match *self.expect_output(output_type) { + Some(OutputAssertion { + expect: ref expected_output, + fuzzy: true, + }) if !observed.contains(expected_output) => { bail!(ErrorKind::OutputMismatch( + output_type.to_string(), + self.cmd.clone(), expected_output.clone(), - stdout.into(), + observed.into(), )); }, - (Some(ref expected_output), false) => { - let differences = Changeset::new(expected_output.trim(), stdout.trim(), "\n"); + Some(OutputAssertion { + expect: ref expected_output, + fuzzy: false, + }) => { + let differences = Changeset::new(expected_output.trim(), observed.trim(), "\n"); if differences.distance > 0 { let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactOutputMismatch(nice_diff)); + bail!(ErrorKind::ExactOutputMismatch( + output_type.to_string(), + self.cmd.clone(), + nice_diff + )); } }, _ => {}, } + Ok(()) + } - let stderr = String::from_utf8_lossy(&output.stderr); - match (self.expect_error_output, self.fuzzy_error_output) { - (Some(ref expected_output), true) if !stderr.contains(expected_output) => { - bail!(ErrorKind::ErrorOutputMismatch( - expected_output.clone(), - stderr.into(), - )); - }, - (Some(ref expected_output), false) => { - let differences = Changeset::new(expected_output.trim(), stderr.trim(), "\n"); - if differences.distance > 0 { - let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactErrorOutputMismatch(nice_diff)); - } - }, - _ => {}, + /// Return a reference to the appropriate output assertion. + fn expect_output(&self, output_type: OutputType) -> &Option { + match output_type { + OutputType::StdOut => &self.expect_stdout, + OutputType::StdErr => &self.expect_stderr, } - - Ok(()) } - /// Execute the command, check the assertions, and panic when they fail + /// Execute the command, check the assertions, and panic when they fail. /// /// # Examples /// @@ -375,45 +482,7 @@ impl Assert { /// ``` pub fn unwrap(self) { if let Err(err) = self.execute() { - panic!("Assert CLI failure:\n{}", err); + panic!("{}", err); } } } - -/// Easily construct an `Assert` with a custom command -/// -/// Make sure to include the crate as `#[macro_use] extern crate assert_cli;` if -/// you want to use this macro. -/// -/// # Examples -/// -/// To test that our very complex cli applications succeeds and prints some -/// text to stdout that contains -/// -/// ```plain -/// No errors whatsoever -/// ``` -/// -/// you would call it like this: -/// -/// ```rust -/// #[macro_use] extern crate assert_cli; -/// # fn main() { -/// assert_cmd!(echo "Launch sequence initiated.\nNo errors whatsoever!\n") -/// .succeeds() -/// .prints("No errors whatsoever") -/// .unwrap(); -/// # } -/// ``` -/// -/// The macro will try to convert its arguments as strings, but is limited by -/// Rust's default tokenizer, e.g., you always need to quote CLI arguments -/// like `"--verbose"`. -#[macro_export] -macro_rules! assert_cmd { - ($($x:tt)+) => {{ - $crate::Assert::command( - &[$(stringify!($x)),*] - ) - }} -} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..e5e1145 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,99 @@ +use std::borrow::Cow; +use rustc_serialize::json::Json; + +/// Easily construct an `Assert` with a custom command. +/// +/// Make sure to include the crate as `#[macro_use] extern crate assert_cli;` if +/// you want to use this macro. +/// +/// # Examples +/// +/// To test that our very complex cli applications succeeds and prints some +/// text to stdout that contains +/// +/// ```plain +/// No errors whatsoever +/// ``` +/// +/// ..., you would call it like this: +/// +/// ```rust +/// #[macro_use] extern crate assert_cli; +/// # fn main() { +/// assert_cmd!(echo "Launch sequence initiated.\nNo errors whatsoever!\n") +/// .succeeds() +/// .prints("No errors whatsoever") +/// .unwrap(); +/// # } +/// ``` +/// +/// The macro will try to convert its arguments as strings, but is limited by +/// Rust's default tokenizer, e.g., you always need to quote CLI arguments +/// like `"--verbose"`. +#[macro_export] +macro_rules! assert_cmd { + ($($x:tt)+) => {{ + $(__assert_single_token_expression!(@CHECK $x);)* + + $crate::Assert::command( + &[$( + $crate::flatten_escaped_string(stringify!($x)).as_ref() + ),*] + ) + }} +} + +/// Deserialize a JSON-encoded `String`. +/// +/// # Panics +/// +/// If `x` can not be decoded as `String`. +#[doc(hidden)] +fn deserialize_json_string(x: &str) -> String { + match Json::from_str(x).expect(&format!("Unable to deserialize `{:?}` as string.", x)) { + Json::String(deserialized) => deserialized, + _ => panic!("Unable to deserialize `{:?}` as string.", x), + } +} + +/// Deserialize a JSON-encoded `String`. +/// +/// # Panics +/// +/// If `x` can not be decoded as `String`. +#[doc(hidden)] +pub fn flatten_escaped_string(x: &str) -> Cow { + if x.starts_with('"') && x.ends_with('"') { + Cow::Owned(deserialize_json_string(x)) + } else { + Cow::Borrowed(x) + } +} + +/// Inspect a single token and decide if it is safe to `stringify!`, without loosing +/// information about whitespaces, to address https://github.com/killercup/assert_cli/issues/22. +/// +/// Call like `__assert_single_token_expression!(@CHECK x)`, where `x` can be any token to check. +/// +/// This macro will only accept single tokens, which parse as expressions, e.g. +/// - strings "foo", r#"foo" +/// - idents `foo`, `foo42` +/// - numbers `42` +/// - chars `'a'` +/// +/// Delimited token trees `{...}` and the like are rejected. Everything thats not an expression +/// will also be rejected. +#[doc(hidden)] +#[macro_export] +macro_rules! __assert_single_token_expression { + // deny `{...}` + (@CHECK {$( $x:tt )*}) => { assert_cmd!(@DENY {$( $x )*}) }; + // deny `(...)` + (@CHECK ($( $x:tt )*)) => { assert_cmd!(@DENY {$( $x )*}) }; + // deny `[...]` + (@CHECK [$( $x:tt )*]) => { assert_cmd!(@DENY {$( $x )*}) }; + // only allow tokens that parse as expression + (@CHECK $x:expr) => { }; + // little helper + (@DENY) => { }; +}