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(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, $($ast: pat)+) => {
30		let source_text = $str;
31		let bump = ::bumpalo::Bump::default();
32		let lexer = css_lexer::Lexer::new(&$atomset, &source_text);
33		let mut parser = $crate::Parser::new(&bump, &source_text, lexer);
34		let result = parser.parse_entirely::<$ty>().with_trivia();
35		if !result.errors.is_empty() {
36			panic!("\n\nParse failed. ({:?}) saw error {:?}", source_text, result.errors[0]);
37		}
38		let mut actual = ::bumpalo::collections::String::new_in(&bump);
39		{
40			let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
41			let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
42			use $crate::ToCursors;
43			result.to_cursors(&mut ordered_sink);
44		}
45		if source_text.trim() != actual.trim() {
46			panic!("\n\nParse failed: did not match expected format:\n\n   parser input: {:?}\n  parser output: {:?}\n", source_text, actual);
47		}
48		#[allow(clippy::redundant_pattern_matching)] // Avoid .clone().unwrap()
49		if !matches!(result.output, Some($($ast)|+)) {
50			panic!(
51					"\n\nParse succeeded but struct did not match given match pattern:\n\n           input: {:?}\n  match pattern: {}\n  parsed struct: {:#?}\n",
52					source_text,
53					stringify!($($ast)|+),
54					result.output.unwrap(),
55				);
56		}
57	};
58	($atomset: path, $ty: ty, $str: literal) => {
59		assert_parse!($atomset, $ty, $str, _);
60	};
61}
62#[cfg(test)]
63pub(crate) use assert_parse;
64
65/// (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
66/// given node. If the parse succeeded this macro will [panic] with a readable failure.
67///
68/// In rare cases it might be necessary to ensure the resulting string _differs_ from the input, for example if a
69/// grammar is serialized in a specific order but allows parsing in any order (many style values do this). In these
70/// cases, a second string can be provided which will be asserted gainst the output instead.
71///
72/// ```
73/// use css_parse::*;
74/// assert_parse_error!(EmptyAtomSet::ATOMS, T![Ident], "1");
75/// ```
76#[macro_export]
77macro_rules! assert_parse_error {
78	($atomset: path, $ty: ty, $str: literal) => {
79		let source_text = $str;
80		let bump = ::bumpalo::Bump::default();
81		let lexer = css_lexer::Lexer::new(&$atomset, source_text);
82		let mut parser = $crate::Parser::new(&bump, source_text, lexer);
83		let result = parser.parse::<$ty>();
84		if parser.at_end() {
85			if let Ok(result) = result {
86				let mut actual = ::bumpalo::collections::String::new_in(&bump);
87				{
88					let mut write_sink = $crate::CursorWriteSink::new(&source_text, &mut actual);
89					let mut ordered_sink = $crate::CursorOrderedSink::new(&bump, &mut write_sink);
90					use $crate::ToCursors;
91					result.to_cursors(&mut ordered_sink);
92				}
93				panic!("\n\nExpected errors but it passed without error.\n\n   parser input: {:?}\n  parser output: {:?}\n       expected: (Error)", source_text, actual);
94			}
95		}
96	};
97}
98#[cfg(test)]
99pub(crate) use assert_parse_error;
100
101/// (Requires feature "testing") Given a Node, and a multiline string, this will expand to code that sets up a parser,
102/// and parses the first line of the given string with the parser. It will then create a second string based on the span
103/// data and append it to the first line of the string, showing what was parsed and where the span rests.
104///
105/// This uses `parse`, as it will be often used in situations where there may be trailing unparsed tokens.
106///
107/// ```
108/// use css_parse::*;
109/// assert_parse_span!(EmptyAtomSet::ATOMS, T![Ident], r#"
110///     an_ident another_ident
111///     ^^^^^^^^
112/// "#);
113/// ```
114#[macro_export]
115macro_rules! assert_parse_span {
116	($atomset: path, $ty: ty, $str: literal) => {
117		let expected = $str;
118		let source_text = expected.lines().find(|line| !line.trim().is_empty()).unwrap_or("");
119		let bump = ::bumpalo::Bump::default();
120		let lexer = css_lexer::Lexer::new(&$atomset, source_text);
121		let mut parser = $crate::Parser::new(&bump, source_text, lexer);
122		let result = parser.parse::<$ty>();
123		match result {
124			Ok(result) => {
125				use $crate::ToSpan;
126				let span = result.to_span();
127				let indent = &source_text[0..span.start().into()];
128				if indent.trim().len() > 0 {
129					panic!(
130						"\n\nParse on {}:{} succeeded but has non-whitespace leading text: {}\n",
131						file!(),
132						line!(),
133						indent
134					);
135				}
136				let actual = format!("\n{}{}\n{}{}\n", indent, source_text, indent, "^".repeat(span.len() as usize));
137				if expected.trim() != actual.trim() {
138					panic!(
139						"\n\nParse on {}:{} succeeded but span ({}) differs:\n\n  expected: {}\n  actual: {}\n",
140						file!(),
141						line!(),
142						span,
143						expected,
144						actual,
145					);
146				}
147			}
148			Err(err) => {
149				panic!("\n\nParse on {}:{} failed. ({:?}) saw error {:?}", file!(), line!(), source_text, err);
150			}
151		}
152	};
153}
154#[cfg(test)]
155pub(crate) use assert_parse_span;