1use proc_macro2::{Ident, Span, TokenStream};
2use quote::{quote, ToTokens, TokenStreamExt};
3use syn::parse::{self, Parse, ParseStream};
4use utils::extract_literal_string;
5
6use crate::catchr_mode::CatchrMode;
7use crate::scope::Scope;
8use crate::section_body::SectionBody;
9use crate::section_item::SectionItem;
10use crate::section_keyword::SectionKeyword;
11use crate::utils;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Section {
15 section_kind: SectionKeyword,
16 name: String,
17 body: SectionBody,
18
19 test_attribute: CatchrMode,
20}
21
22impl Section {
23 pub fn new(
24 section_kind: SectionKeyword,
25 name: impl ToString,
26 body: SectionBody,
27 ) -> Self {
28 Self {
29 section_kind,
30 name: name.to_string(),
31 body,
32 test_attribute: CatchrMode::Regular,
33 }
34 }
35
36 pub fn with_mode(mut self, test_attribute: CatchrMode) -> Self {
37 self.test_attribute = test_attribute;
38 self.body = self.body.with_mode(test_attribute);
39 self
40 }
41
42 fn quote_name(&self) -> Ident {
43 let name = utils::escape_name(&self.name);
44 let kind = self.section_kind.to_name();
45
46 let name = if kind.is_empty() {
47 name
48 } else {
49 format!("{}_{}", kind, name)
50 };
51
52 Ident::new(&name, Span::call_site())
53 }
54
55 pub fn quote_inner(&self, scope: Scope) -> TokenStream {
56 let mut token_stream = TokenStream::default();
57
58 self.to_tokens_inner(scope, &mut token_stream);
59
60 token_stream
61 }
62
63 pub fn peek(input: ParseStream) -> bool {
64 SectionKeyword::peek(input)
65 }
66
67 fn to_tokens_inner(&self, scope: Scope, tokens: &mut TokenStream) {
68 if self.body.is_top_level() {
69 let my_stmts: Vec<_> =
70 self.body.items().iter().filter_map(|i| i.stmt()).collect();
71
72 let name = self.quote_name();
73
74 let inner = scope.quote_with(&my_stmts);
75
76 match self.test_attribute {
77 CatchrMode::Regular => tokens.append_all(quote! {
78 #[test]
79 fn #name() {
80 #inner
81 }
82 }),
83 CatchrMode::Tokio => tokens.append_all(quote! {
84 #[tokio::test]
85 async fn #name() {
86 #inner
87 }
88 }),
89 }
90
91 return;
92 }
93
94 let mut stream = vec![];
95
96 for (idx, item) in self.body.items().iter().enumerate() {
97 if let SectionItem::Sep(section) = item {
98 let sb = self.body.get_stmts_before(idx);
99 let sa = self.body.get_stmts_after(idx);
100
101 let mut scope = scope.clone();
102 scope.push_mut(&sb, &sa);
103
104 let inner = section.quote_inner(scope);
105
106 stream.push(inner);
107 }
108 }
109
110 let name = self.quote_name();
111
112 tokens.append_all(quote! {
113 mod #name {
114 use super::*;
115
116 #(#stream)*
117 }
118 });
119 }
120}
121
122impl ToTokens for Section {
123 fn to_tokens(&self, tokens: &mut TokenStream) {
124 let scope = Scope::empty();
125
126 self.to_tokens_inner(scope, tokens);
127 }
128}
129
130impl Parse for Section {
131 fn parse(input: ParseStream) -> parse::Result<Self> {
132 let section_keyword: SectionKeyword = input.parse()?;
133 let name: syn::Lit = input.parse()?;
134 let name = extract_literal_string(name).ok_or_else(|| {
135 parse::Error::new(Span::call_site(), "Invalid section literal")
136 })?;
137
138 let content;
139 syn::braced!(content in input);
140 let inner_body = content.parse::<SectionBody>()?;
141
142 Ok(Section::new(section_keyword, name, inner_body))
143 }
144}
145
146#[cfg(test)]
147mod tests {
148
149 use super::*;
150
151 mod regular {
152 use test_case::test_case;
153
154 use super::*;
155
156 #[test_case(
157 r#"
158 section "tests" {
159 let x = 1;
160 when "hello" {
161 assert!(true);
162 then "whatever" {
163 assert!(true);
164 }
165 }
166
167 assert_eq!(x, 1);
168 }
169 "#,
170 quote!(
171 mod section_tests {
172 use super::*;
173
174 mod when_hello {
175 use super::*;
176
177 #[test]
178 fn then_whatever() {
179 {
180 let x = 1;
181 {
182 assert!(true);
183 {
184 assert!(true);
185 }
186 }
187 assert_eq!(x, 1);
188 }
189 }
190 }
191 }
192 )
193 )]
194 #[test_case(
195 r#"
196 section "tests" {
197 assert!(1 == 1);
198
199 case "one" {
200 assert!(2 == 2);
201 }
202
203 assert!(3 == 3);
204
205 case "two" {
206 assert!(4 == 4);
207 }
208
209 assert!(5 == 5);
210 }
211 "#,
212 quote!(
213 mod section_tests {
214 use super::*;
215
216 #[test]
217 fn case_one() {
218 {
219 assert!(1 == 1);
220 {
221 assert!(2 == 2);
222 }
223 assert!(3 == 3);
224 assert!(5 == 5);
225 }
226 }
227
228 #[test]
229 fn case_two() {
230 {
231 assert!(1 == 1);
232 assert!(3 == 3);
233 {
234 assert!(4 == 4);
235 }
236 assert!(5 == 5);
237 }
238 }
239 }
240 )
241 )]
242 fn parse_and_quote(s: &str, exp: TokenStream) {
243 let section = syn::parse_str::<Section>(s).unwrap();
244 let section = section.to_token_stream();
245
246 assert_eq!(exp.to_string(), section.to_string());
247 }
248 }
249
250 mod tokio {
251 use test_case::test_case;
252
253 use super::*;
254
255 #[test_case(
256 r#"
257 section "tests" {
258 let x = 1;
259 when "hello" {
260 assert!(true);
261 then "whatever" {
262 assert!(true);
263 }
264 }
265
266 assert_eq!(x, 1);
267 }
268 "#,
269 quote!(
270 mod section_tests {
271 use super::*;
272
273 mod when_hello {
274 use super::*;
275
276 #[tokio::test]
277 async fn then_whatever() {
278 {
279 let x = 1;
280 {
281 assert!(true);
282 {
283 assert!(true);
284 }
285 }
286 assert_eq!(x, 1);
287 }
288 }
289 }
290 }
291 )
292 )]
293 #[test_case(
294 r#"
295 section "tests" {
296 assert!(1 == 1);
297
298 case "one" {
299 assert!(2 == 2);
300 }
301
302 assert!(3 == 3);
303
304 case "two" {
305 assert!(4 == 4);
306 }
307
308 assert!(5 == 5);
309 }
310 "#,
311 quote!(
312 mod section_tests {
313 use super::*;
314
315 #[tokio::test]
316 async fn case_one() {
317 {
318 assert!(1 == 1);
319 {
320 assert!(2 == 2);
321 }
322 assert!(3 == 3);
323 assert!(5 == 5);
324 }
325 }
326
327 #[tokio::test]
328 async fn case_two() {
329 {
330 assert!(1 == 1);
331 assert!(3 == 3);
332 {
333 assert!(4 == 4);
334 }
335 assert!(5 == 5);
336 }
337 }
338 }
339 )
340 )]
341 fn parse_and_quote(s: &str, exp: TokenStream) {
342 let section = syn::parse_str::<Section>(s).unwrap();
343 let section =
344 section.with_mode(CatchrMode::Tokio).to_token_stream();
345
346 assert_eq!(exp.to_string(), section.to_string());
347 }
348 }
349}