1use std::{
7 cmp::Ordering,
8 env::var,
9 fs::DirEntry,
10 path::PathBuf,
11 format_args as fmt,
12};
13
14use proc_macro2::{
15 Ident,
16 Literal,
17 Span,
18 TokenStream,
19};
20use smallvec::SmallVec;
21use syn::{
22 ExprLit,
23 FnArg,
24 ItemFn,
25 Lit,
26 parse2,
27 parse_str,
28 Pat,
29 PatIdent,
30 PatType,
31 Signature,
32 Type,
33 Generics,
34};
35
36use quote::{
37 quote,
38 ToTokens,
39};
40
41mod traits;
42
43use self::traits::*;
44
45const INPUT_TXT: &str = "input.txt";
46const INPUT_RS: &str = "input.rs";
47const INPUT_BIN: &str = "input.bin";
48
49#[doc(hidden)]
50pub fn make_snapshots(path_attr: &TokenStream, item: &TokenStream) -> Result<TokenStream, TokenStream> {
51 let (
52 name,
53 Generics {
54 lt_token: generic_lt,
55 gt_token: generic_gt,
56 params: generic_params,
57 where_clause: generic_where,
58 },
59 (param_name, param_type),
60 ) = pull_function_description(item.clone())?;
61
62 let actual_file_name = {
63 let mut base_name = name.to_string();
64 base_name.push_str(".actual.txt");
65 base_name
66 };
67 let expected_file_name = {
68 let mut base_name = name.to_string();
69 base_name.push_str(".txt");
70 base_name
71 };
72
73 if expected_file_name == INPUT_TXT {
74 return ().compile_error(fmt!("Cannot use that name, as it conflicts with {} detection", INPUT_TXT))
75 }
76
77 let base_name = name.to_token_stream();
78
79 let path = {
80 let mut path = PathBuf::new();
81 path.push(var("CARGO_MANIFEST_DIR").compile_err("No manifest directory env")?);
82 path.push(
83 if let Lit::Str(string) =
84 parse2::<ExprLit>(path_attr.clone())
85 .compile_error(fmt!("Expected literal path in attribute, received: {}\n\n", path_attr))?
86 .lit
87 {
88 string.value()
89 } else {
90 return ().compile_error(fmt!("Expected literal path: {}", path_attr))
91 }
92 );
93 path
94 };
95
96 let tag: TokenStream = "#[test]".parse().compile_err("Failed to init tag")?;
97 let supers: TokenStream = "super::".parse().compile_err("Failed to init supers")?;
98
99 let outputs = nested_fixtures(
100 sort_dir(path
101 .read_dir()
102 .compile_error(fmt!("Failed to read {:?}", path))?
103 )
104 .into_iter()
105 .map(|result|
106 result.compile_error(fmt!("Failed to read in {:?}", path))
107 ),
108 &TokenStream::new(),
109 &Params {
110 tag,
111 base_name,
112 supers,
113 actual_file_name,
114 expected_file_name,
115 }
116 );
117
118 Ok(quote! {
121 fn #name #generic_lt #generic_params #generic_gt (mut #param_name: (
122 impl std::ops::Fn(&mut std::option::Option<#param_type>) + std::panic::RefUnwindSafe + std::panic::UnwindSafe,
123 &'static str,
124 &'static str,
125 )) #generic_where {
126 #item
127
128 let (to_call, (provider, expected_file, actual_file)) =
129 (&#name, #param_name);
130
131 let result = format!(
132 "{:#?}\n",
133 std::panic::catch_unwind(
134 move || {
135 let mut temp = std::option::Option::None;
136 provider(&mut temp);
137 to_call(temp.unwrap())
138 }
139 ).map_err(|err| err
140 .downcast::<String>()
141 .or_else(|err|
142 if let Some(string) = err.downcast_ref::<&str>() {
143 std::result::Result::Ok(std::boxed::Box::new(string.to_string()))
144 } else {
145 std::result::Result::Err(("<!String> Panic", err))
146 }
147 )
148 .map(|ok| ("<String> Panic", ok))
149 )
150 );
151 if std::path::Path::new(expected_file).is_file() {
152 let expected = std::fs::read_to_string(expected_file)
153 .unwrap_or_else(|err|
154 panic!("Reading expected from {}: {:?}", expected_file, err)
155 );
156 assert_eq!(result, expected)
157 } else {
158 std::fs::write(actual_file, result.as_bytes())
159 .unwrap_or_else(|err|
160 panic!("Writing actual to {}: {:?}", actual_file, err)
161 );
162 panic!("No expected value set: {}", actual_file)
163 }
164 }
165
166 mod #name {
167 #outputs
168 }
169 })
170}
171
172fn pull_function_description(item: TokenStream) -> Result<(Ident, Generics, (Ident, Type)), TokenStream> {
173 let Signature {
174 ident: name,
175 inputs: param,
176 generics,
177 ..
178 } = parse2::<ItemFn>(item.clone())
179 .compile_error(fmt!("Expected attribute must be on a function, received: {}\n\n", item))?
180 .sig;
181 let param: SmallVec<[FnArg; 1]> = param.into_iter().collect();
182 let param = match param.into_inner() {
183 Ok([param]) => param,
184 Err(ref param) if param.is_empty() => return ().compile_err("No input parameter"),
185 Err(param) => return ().compile_error(fmt!(
186 "Expected one parameter, received {}",
187 param
188 .into_iter()
189 .map(FnArg::into_token_stream)
190 .flatten()
191 .collect::<TokenStream>()
192 )),
193 };
194 let (param_type, param_name) = match param {
195 FnArg::Typed(PatType { pat, ty, .. }) => (*ty, *pat),
196 param => return ().compile_error(fmt!("Unexpected self in {}", param.into_token_stream())),
197 };
198 let param_name = match param_name {
199 Pat::Ident(PatIdent { ident, .. }) => ident,
200 pat => return ().compile_error(fmt!("Expected parameter, received {}", pat.into_token_stream())),
201 };
202 if format!("{}", param_name) == format!("{}", name) {
203 return ().compile_error(fmt!("Function {} may not share name with its parameter", name));
204 }
205 Ok((name, generics, (param_name, param_type)))
206}
207
208struct Params {
209 tag: TokenStream,
210 base_name: TokenStream,
211 supers: TokenStream,
212 actual_file_name: String,
213 expected_file_name: String,
214}
215
216fn nested_fixtures(
217 folders: impl IntoIterator<Item=Result<DirEntry, TokenStream>>,
218 super_chain: &TokenStream,
219 params: &Params,
220) -> TokenStream {
221 let Params {
222 tag,
223 base_name,
224 supers,
225 actual_file_name,
226 expected_file_name,
227 } = params;
228 let super_chain = {
229 let mut super_chain = super_chain.clone();
230 supers.to_tokens(&mut super_chain);
231 super_chain
232 };
233 folders
234 .into_iter()
235 .map(|result| result.and_then(|fixture: DirEntry| {
236 let fixture_path = fixture
237 .path()
238 .canonicalize()
239 .compile_error(fmt!("Failed to canonicalize fixtures: {:?}", fixture))?;
240 let fixture_name = parse_str::<Ident>(fixture
241 .file_name()
242 .to_str()
243 .compile_error(fmt!("Failed to convert filename to utf8 of {:?}", fixture))?,
244 ).compile_error(fmt!("Failed to convert filename of {:?} into rust identifier", fixture_path))?;
245
246 let mut input_rs = None;
247 let mut input_txt = None;
248 let mut input_bin = None;
249 let mut folders: Option<Vec<_>> = None;
250
251 for file in sort_dir(fixture_path
252 .read_dir()
253 .compile_error(fmt!("Failed to read fixture directory {:?}", fixture_path))?
254 ) {
255 macro_rules! push_err {($ex:expr) => {{
256 match $ex {
257 Err(e) => {
258 folders.get_or_insert_with(Vec::new).push(Err(e));
259 continue;
260 },
261 Ok(value) => value,
262 }
263 }};}
264
265 let file: DirEntry = push_err!(
266 file.compile_error(fmt!("Failed to get DirEntry in {:?}", fixture_path))
267 );
268
269 if push_err!(
270 file.file_type().compile_error(fmt!("Bad file type of {:?}", file))
271 ).is_dir() {
272 folders
273 .get_or_insert_with(Vec::new)
274 .push(Ok(file));
275 continue;
276 }
277
278 let name = file.file_name();
279 let name = push_err!(
280 name.to_str().compile_error(fmt!("Unresolvable file name"))
281 );
282
283 let file_pointer = match name {
284 INPUT_RS => &mut input_rs,
285 INPUT_TXT => &mut input_txt,
286 INPUT_BIN => &mut input_bin,
287 _ => continue,
288 };
289 *file_pointer = Some(file);
290 }
291
292 match (
293 folders.as_ref().map_or(
294 true,
295 |folders|
296 folders.iter().any(Result::is_err),
297 ),
298 &input_rs,
299 &input_bin,
300 &input_txt,
301 ) {
302 (true, None, Some(_), None) => {},
305 (true, None, None, Some(_)) => {},
306 (true, Some(_), None, None) => {},
307 (false, None, None, None) => {},
309 _ => folders
314 .get_or_insert_with(Vec::new)
315 .push(().compile_error(fmt!(
316 "Expected sub-directories or exactly one of {}, {}, or {} in {:?}",
317 INPUT_RS,
318 INPUT_BIN,
319 INPUT_TXT,
320 fixture_path,
321 ))),
322 }
323
324 let (include, file) = match (folders, input_rs, input_bin, input_txt) {
325 (Some(folders), _, _, _) => {
327 let fixtures = nested_fixtures(
328 folders,
329 &super_chain,
330 params,
331 );
332 return Ok(quote! {
333 mod #fixture_name {
334 #fixtures
335 }
336 })
337 },
338 (None, Some(file), None, None) => ("include", file),
340 (None, None, Some(file), None) => ("include_bytes", file),
342 (None, None, None, Some(file)) => ("include_str", file),
344 _ => unreachable!(),
346 };
347 let include= Ident::new(include, Span::call_site());
349
350 let make_literal = |path: PathBuf| path
351 .to_str()
352 .compile_error(fmt!("Failed to get utf8 string from {:?}", path))
353 .map(Literal::string);
354 let input_literal = make_literal(file.path())?;
355 let actual_literal = make_literal(fixture_path.join(actual_file_name))?;
356 let expected_literal = make_literal(fixture_path.join(expected_file_name))?;
357
358 Ok(quote! {
359 #tag
360 fn #fixture_name() {
361 #super_chain #base_name((
362 |#fixture_name: &mut std::option::Option<_>| {
363 #fixture_name.replace(#include!(#input_literal));
364 },
365 #expected_literal,
366 #actual_literal,
367 ))
368 }
369 })
370 }))
371 .map(EitherResult::either)
372 .collect()
373}
374
375fn sort_dir<T>(iter: impl IntoIterator<Item=Result<DirEntry, T>>) -> impl IntoIterator<Item=Result<DirEntry, T>> {
376 let mut vec: Vec<_> = iter.into_iter().collect();
377 vec.sort_by(|left, right| match (left, right) {
378 (Ok(left), Ok(right)) => match (left.file_name().to_str(), right.file_name().to_str()) {
379 (Some(left), Some(right)) => left.cmp(right),
380 (None, None) => Ordering::Equal,
381 (Some(_), None) => Ordering::Greater,
382 (None, Some(_)) => Ordering::Less,
383 },
384 (Err(_), Err(_)) => Ordering::Equal,
385 (Ok(_), Err(_)) => Ordering::Greater,
386 (Err(_), Ok(_)) => Ordering::Less,
387 });
388 vec
389}