Skip to main content

css_parse/
test_helpers.rs

1/// (Requires feature "testing") Given a Node, and a string, this will expand to code that sets up a parser, and parses the given string against the
2/// given node. If the parse failed this macro will [panic] with a readable failure. It then writes the result out using
3/// [crate::CursorWriteSink], writing the parsed Node back out to a string. If resulting string from the given string, then the
4/// macro will [panic] with a readable failure.
5///
6/// ```
7/// use css_parse::*;
8/// assert_parse!(EmptyAtomSet::ATOMS, T![Ident], "foo");
9/// ```
10///
11/// For more complex types (for example enum variants), you might want to assert that the given AST
12/// node matches an expected pattern (for example, one enum variant was chosen over another). In
13/// these cases, passing the match pattern as the third (or fourth) argument will assert that the
14/// parsed output struct matches the given pattern:
15///
16/// ```
17/// use css_parse::*;
18/// use csskit_derives::*;
19/// #[derive(Peek, Parse, ToCursors, Debug)]
20/// enum IdentOrNumber {
21///     Ident(T![Ident]),
22///     Number(T![Number]),
23/// }
24/// assert_parse!(EmptyAtomSet::ATOMS, IdentOrNumber, "foo", IdentOrNumber::Ident(_));
25/// assert_parse!(EmptyAtomSet::ATOMS, IdentOrNumber, "12", IdentOrNumber::Number(_));
26/// ```
27#[macro_export]
28macro_rules! assert_parse {
29	($atomset: path, $ty: ty, $str: literal, $expected: literal) => {
30		{
31			let source_text = $str;
32			let bump = ::bumpalo::Bump::default();
33			let lexer = css_lexer::Lexer::new(&$atomset, &source_text);
34			let mut parser = $crate::Parser::new(&bump, &source_text, lexer);
35			let result = parser.parse_entirely::<$ty>().with_trivia();
36			if !result.errors.is_empty() {
37				panic!("\n\nParse failed. ({:?}) saw error {:?}", source_text, result.errors[0]);
38			}
39			let mut actual = ::bumpalo::collections::String::new_in(&bump);
40			{
41				let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
42				let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
43				use $crate::ToCursors;
44				result.to_cursors(&mut ordered_sink);
45			}
46			if $expected.trim() != actual.trim() {
47				panic!("\n\nParse failed: did not match expected output:\n\n   parser input: {:?}\n  parser output: {:?}\n       expected: {:?}\n", source_text, actual, $expected);
48			}
49		}
50	};
51	($atomset: path, $ty: ty, $str: literal) => {
52		assert_parse!($atomset, $ty, $str, _);
53	};
54	($atomset: path, $ty: ty, $str: literal, |$node: ident| $body: expr) => {
55		{
56			let source_text = $str;
57			let bump = ::bumpalo::Bump::default();
58			let lexer = css_lexer::Lexer::new(&$atomset, &source_text);
59			let mut parser = $crate::Parser::new(&bump, &source_text, lexer);
60			let result = parser.parse_entirely::<$ty>().with_trivia();
61			if !result.errors.is_empty() {
62				panic!("\n\nParse failed. ({:?}) saw error {:?}", source_text, result.errors[0]);
63			}
64			let mut actual = ::bumpalo::collections::String::new_in(&bump);
65			{
66				let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
67				let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
68				use $crate::ToCursors;
69				result.to_cursors(&mut ordered_sink);
70			}
71			if source_text.trim() != actual.trim() {
72				panic!("\n\nParse failed: did not match expected format:\n\n   parser input: {:?}\n  parser output: {:?}\n", source_text, actual);
73			}
74			let $node = result.output.expect("parse succeeded");
75			$body
76		}
77	};
78	($atomset: path, $ty: ty, $str: literal, $($ast: pat)+) => {
79		{
80			let source_text = $str;
81			let bump = ::bumpalo::Bump::default();
82			let lexer = css_lexer::Lexer::new(&$atomset, &source_text);
83			let mut parser = $crate::Parser::new(&bump, &source_text, lexer);
84			if !parser.peek::<$ty>() {
85				panic!("\n\nParse failed because Type didn't peek!\n\n   parser input: {:?}\n", source_text);
86			}
87			let result = parser.parse_entirely::<$ty>().with_trivia();
88			if !result.errors.is_empty() {
89				panic!("\n\nParse failed. ({:?}) saw error {:?}", source_text, result.errors[0]);
90			}
91			let mut actual = ::bumpalo::collections::String::new_in(&bump);
92			{
93				let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
94				let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
95				use $crate::ToCursors;
96				result.to_cursors(&mut ordered_sink);
97			}
98			if source_text.trim() != actual.trim() {
99				panic!("\n\nParse failed: did not match expected format:\n\n   parser input: {:?}\n  parser output: {:?}\n", source_text, actual);
100			}
101			#[allow(clippy::redundant_pattern_matching)] // Avoid .clone().unwrap()
102			if !matches!(result.output, Some($($ast)|+)) {
103				panic!(
104					"\n\nParse succeeded but struct did not match given match pattern:\n\n           input: {:?}\n  match pattern: {}\n  parsed struct: {:#?}\n",
105					source_text,
106					stringify!($($ast)|+),
107					result.output.unwrap(),
108				);
109			}
110		}
111	};
112}
113#[cfg(test)]
114pub(crate) use assert_parse;
115
116/// (Requires feature "testing") Given a Node, and a string, this will expand to code that sets up a parser, and parses the given string against the
117/// given node. If the parse succeeded this macro will [panic] with a readable failure.
118///
119/// In rare cases it might be necessary to ensure the resulting string _differs_ from the input, for example if a
120/// grammar is serialized in a specific order but allows parsing in any order (many style values do this). In these
121/// cases, a second string can be provided which will be asserted gainst the output instead.
122///
123/// ```
124/// use css_parse::*;
125/// assert_parse_error!(EmptyAtomSet::ATOMS, T![Ident], "one two");
126/// ```
127#[macro_export]
128macro_rules! assert_parse_error {
129	($atomset: path, $ty: ty, $str: literal) => {
130		let source_text = $str;
131		let bump = ::bumpalo::Bump::default();
132		let lexer = css_lexer::Lexer::new(&$atomset, source_text);
133		let mut parser = $crate::Parser::new(&bump, source_text, lexer);
134		if !parser.peek::<$ty>() {
135			panic!("\n\n.\n\nPeek returned false, please don't use `assert_parse_error` - use `assert_peek_false` instead: {:?}", source_text);
136		}
137		let result = parser.parse::<$ty>();
138		if parser.at_end() {
139			if let Ok(result) = result {
140				let mut actual = ::bumpalo::collections::String::new_in(&bump);
141				{
142					let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
143					let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
144					use $crate::ToCursors;
145					result.to_cursors(&mut ordered_sink);
146				}
147				panic!("\n\nExpected errors but it passed without error.\n\n   parser input: {:?}\n  parser output: {:?}\n       expected: (Error)", source_text, actual);
148			}
149		}
150	};
151}
152#[cfg(test)]
153pub(crate) use assert_parse_error;
154
155/// (Requires feature "testing") Given a Node, and a string, this will expand to code that sets up a parser, and checks
156/// that the Node returns false when Peeking on the node. It _also_ parses using the node, to ensure that the Parse
157/// causes an error, confirming that Peek doesn't contradict Parse. If the parse succeeded this macro will [panic] with
158/// a readable failure.
159///
160/// ```
161/// use css_parse::*;
162/// assert_peek_false!(EmptyAtomSet::ATOMS, T![Ident], "1");
163/// ```
164#[macro_export]
165macro_rules! assert_peek_false {
166	($atomset: path, $ty: ty, $str: literal) => {
167		let source_text = $str;
168		let bump = ::bumpalo::Bump::default();
169		let lexer = css_lexer::Lexer::new(&$atomset, source_text);
170		let mut parser = $crate::Parser::new(&bump, source_text, lexer);
171		if parser.peek::<$ty>() {
172			panic!("\n\n.\n\nPeek returned true! You might want `assert_parse_error` instead: {:?}", source_text);
173		}
174		let result = parser.parse::<$ty>();
175		if parser.at_end() {
176			if let Ok(result) = result {
177				let mut actual = ::bumpalo::collections::String::new_in(&bump);
178				{
179					let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
180					let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
181					use $crate::ToCursors;
182					result.to_cursors(&mut ordered_sink);
183				}
184				panic!("\n\nExpected errors but it passed without error.\n\n   parser input: {:?}\n  parser output: {:?}\n       expected: (Error)", source_text, actual);
185			}
186		}
187	};
188}
189#[cfg(test)]
190pub(crate) use assert_peek_false;
191
192/// (Requires feature "testing") Given a Node, and a multiline string, this will expand to code that sets up a parser,
193/// and parses the first line of the given string with the parser. It will then create a second string based on the span
194/// data and append it to the first line of the string, showing what was parsed and where the span rests.
195///
196/// This uses `parse`, as it will be often used in situations where there may be trailing unparsed tokens.
197///
198/// ```
199/// use css_parse::*;
200/// assert_parse_span!(EmptyAtomSet::ATOMS, T![Ident], r#"
201///     an_ident another_ident
202///     ^^^^^^^^
203/// "#);
204/// ```
205#[macro_export]
206macro_rules! assert_parse_span {
207	($atomset: path, $ty: ty, $str: literal) => {
208		let expected = $str;
209		let source_text = expected.lines().find(|line| !line.trim().is_empty()).unwrap_or("");
210		let bump = ::bumpalo::Bump::default();
211		let lexer = css_lexer::Lexer::new(&$atomset, source_text);
212		let mut parser = $crate::Parser::new(&bump, source_text, lexer);
213		let result = parser.parse::<$ty>();
214		match result {
215			Ok(result) => {
216				use $crate::ToSpan;
217				let span = result.to_span();
218				let indent = &source_text[0..span.start().into()];
219				if indent.trim().len() > 0 {
220					panic!(
221						"\n\nParse on {}:{} succeeded but has non-whitespace leading text: {}\n",
222						file!(),
223						line!(),
224						indent
225					);
226				}
227				let actual = format!("\n{}{}\n{}{}\n", indent, source_text, indent, "^".repeat(span.len() as usize));
228				if expected.trim() != actual.trim() {
229					panic!(
230						"\n\nParse on {}:{} succeeded but span ({}) differs:\n\n  expected: {}\n  actual: {}\n",
231						file!(),
232						line!(),
233						span,
234						expected,
235						actual,
236					);
237				}
238			}
239			Err(err) => {
240				panic!("\n\nParse on {}:{} failed. ({:?}) saw error {:?}", file!(), line!(), source_text, err);
241			}
242		}
243	};
244}
245#[cfg(test)]
246pub(crate) use assert_parse_span;