1use std::env;
2use std::ffi::OsStr;
3use std::fs;
4use std::io::Write;
5use std::iter;
6use std::path::{Path, PathBuf};
7
8use crate::cargo;
9use crate::dependencies::{self, Dependency};
10use crate::features;
11use crate::manifest::{Bin, Build, Config, Manifest, Name, Package, Workspace};
12use crate::message::{message_different, message_expansion_error};
13use crate::rustflags;
14use crate::{error::Error, error::Result};
15use syn::punctuated::Punctuated;
16use syn::{Item, Meta, Token};
17
18const EXPANDED_RS_SUFFIX: &str = "expanded.rs";
20
21#[derive(Debug)]
22pub(crate) struct Project {
23 pub dir: PathBuf,
24 source_dir: PathBuf,
25 pub inner_target_dir: PathBuf,
27 pub name: String,
28 pub features: Option<Vec<String>>,
29 workspace: PathBuf,
30 overwrite: bool,
31}
32
33impl Drop for Project {
36 fn drop(&mut self) {
37 if let Err(e) = fs::remove_dir_all(&self.dir) {
38 eprintln!(
39 "Failed to cleanup the directory `{}`: {}",
40 self.dir.to_string_lossy(),
41 e
42 );
43 }
44 }
45}
46
47pub fn expand(path: impl AsRef<Path>) {
58 run_tests(
59 path,
60 ExpansionBehavior::RegenerateFiles,
61 Option::<Vec<String>>::None,
62 );
63}
64
65pub fn expand_args<I, S>(path: impl AsRef<Path>, args: I)
69where
70 I: IntoIterator<Item = S> + Clone,
71 S: AsRef<OsStr>,
72{
73 run_tests(path, ExpansionBehavior::RegenerateFiles, Some(args));
74}
75
76pub fn expand_without_refresh(path: impl AsRef<Path>) {
90 run_tests(
91 path,
92 ExpansionBehavior::ExpectFiles,
93 Option::<Vec<String>>::None,
94 );
95}
96
97pub fn expand_without_refresh_args<I, S>(path: impl AsRef<Path>, args: I)
101where
102 I: IntoIterator<Item = S> + Clone,
103 S: AsRef<OsStr>,
104{
105 run_tests(path, ExpansionBehavior::ExpectFiles, Some(args));
106}
107
108#[derive(Debug, Copy, Clone)]
109enum ExpansionBehavior {
110 RegenerateFiles,
111 ExpectFiles,
112}
113
114fn run_tests<I, S>(path: impl AsRef<Path>, expansion_behavior: ExpansionBehavior, args: Option<I>)
115where
116 I: IntoIterator<Item = S> + Clone,
117 S: AsRef<OsStr>,
118{
119 let tests = expand_globs(&path)
120 .into_iter()
121 .filter(|t| !t.test.to_string_lossy().ends_with(EXPANDED_RS_SUFFIX))
122 .collect::<Vec<_>>();
123
124 let len = tests.len();
125 println!("Running {} macro expansion tests", len);
126
127 let project = prepare(&tests).unwrap_or_else(|err| {
128 panic!("prepare failed: {:#?}", err);
129 });
130
131 let mut failures = 0;
132 for test in tests {
133 let path = test.test.display();
134 let expanded_path = test.test.with_extension(EXPANDED_RS_SUFFIX);
135
136 match test.run(&project, expansion_behavior, &args) {
137 Ok(outcome) => match outcome {
138 ExpansionOutcome::Same => {
139 let _ = writeln!(std::io::stdout(), "{} - ok", path);
140 }
141
142 ExpansionOutcome::Different(a, b) => {
143 message_different(&path.to_string(), &a, &b);
144 failures += 1;
145 }
146
147 ExpansionOutcome::Update => {
148 let _ = writeln!(std::io::stderr(), "{} - refreshed", expanded_path.display());
149 }
150
151 ExpansionOutcome::ExpandError(msg) => {
152 message_expansion_error(msg);
153 failures += 1;
154 }
155 ExpansionOutcome::NoExpandedFileFound => {
156 let _ = writeln!(
157 std::io::stderr(),
158 "{} is expected but not found",
159 expanded_path.display()
160 );
161 failures += 1;
162 }
163 },
164
165 Err(e) => {
166 eprintln!("Error: {:#?}", e);
167 failures += 1;
168 }
169 }
170 }
171
172 if failures > 0 {
173 eprintln!("\n\n");
174 panic!("{} of {} tests failed", failures, len);
175 }
176}
177
178fn prepare(tests: &[ExpandedTest]) -> Result<Project> {
179 let metadata = cargo::metadata()?;
180 let target_dir = metadata.target_directory;
181 let workspace = metadata.workspace_root;
182
183 let crate_name = env::var("CARGO_PKG_NAME").map_err(|_| Error::PkgName)?;
184
185 let source_dir = env::var_os("CARGO_MANIFEST_DIR")
186 .map(PathBuf::from)
187 .ok_or(Error::ManifestDir)?;
188
189 let features = features::find();
190
191 let overwrite = match env::var_os("MACROTEST") {
192 Some(ref v) if v == "overwrite" => true,
193 Some(v) => return Err(Error::UnrecognizedEnv(v)),
194 None => false,
195 };
196
197 let random_string: String = iter::repeat_with(fastrand::alphanumeric).take(42).collect();
200 let dir = path!(target_dir / "tests" / crate_name / random_string);
201 if dir.exists() {
202 fs::remove_dir_all(&dir)?;
206 }
207
208 let inner_target_dir = path!(target_dir / "tests" / "macrotest");
209
210 let mut project = Project {
211 dir,
212 source_dir,
213 inner_target_dir,
214 name: format!("{}-tests", crate_name),
215 features,
216 workspace,
217 overwrite,
218 };
219
220 let manifest = make_manifest(crate_name, &project, tests)?;
221 let manifest_toml = toml::ser::to_string(&manifest)?;
222
223 let config = make_config();
224 let config_toml = toml::ser::to_string(&config)?;
225
226 if let Some(enabled_features) = &mut project.features {
227 enabled_features.retain(|feature| manifest.features.contains_key(feature));
228 }
229
230 fs::create_dir_all(path!(project.dir / ".cargo"))?;
231 fs::write(path!(project.dir / ".cargo" / "config.toml"), config_toml)?;
232 fs::write(path!(project.dir / "Cargo.toml"), manifest_toml)?;
233 fs::write(path!(project.dir / "main.rs"), b"fn main() {}\n")?;
234
235 let source_lockfile = path!(project.workspace / "Cargo.lock");
236 match fs::copy(source_lockfile, path!(project.dir / "Cargo.lock")) {
237 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
238 otherwise => otherwise,
239 }?;
240
241 fs::create_dir_all(&project.inner_target_dir)?;
242
243 cargo::build_dependencies(&project)?;
244
245 Ok(project)
246}
247
248fn make_manifest(
249 crate_name: String,
250 project: &Project,
251 tests: &[ExpandedTest],
252) -> Result<Manifest> {
253 let source_manifest = dependencies::get_manifest(&project.source_dir);
254 let workspace_manifest = dependencies::get_workspace_manifest(&project.workspace);
255
256 let features = source_manifest
257 .features
258 .iter()
259 .map(|(feature, source_deps)| {
260 let enable = format!("{}/{}", crate_name, feature);
261 let mut deps = vec![enable];
262 deps.extend(
263 source_deps
264 .iter()
265 .filter(|dep| dep.starts_with("dep:"))
266 .cloned(),
267 );
268 (feature.clone(), deps)
269 })
270 .collect();
271
272 let mut manifest = Manifest {
273 cargo_features: source_manifest.cargo_features.clone(),
274 package: Package {
275 name: project.name.clone(),
276 version: "0.0.0".to_owned(),
277 edition: source_manifest.package.edition,
278 publish: false,
279 },
280 features,
281 dependencies: std::collections::BTreeMap::new(),
282 bins: Vec::new(),
283 workspace: Some(Workspace {
284 package: crate::manifest::WorkspacePackage {
285 edition: workspace_manifest.workspace.package.edition,
286 },
287 dependencies: workspace_manifest.workspace.dependencies,
288 }),
289 patch: workspace_manifest.patch,
292 replace: workspace_manifest.replace,
293 };
294
295 manifest.dependencies.extend(source_manifest.dependencies);
296 manifest
297 .dependencies
298 .extend(source_manifest.dev_dependencies);
299 manifest.dependencies.insert(
300 crate_name,
301 Dependency {
302 version: None,
303 path: Some(project.source_dir.clone()),
304 default_features: false,
305 features: Vec::new(),
306 workspace: false,
307 rest: std::collections::BTreeMap::new(),
308 },
309 );
310
311 manifest.bins.push(Bin {
312 name: Name(project.name.to_owned()),
313 path: Path::new("main.rs").to_owned(),
314 });
315
316 for expanded in tests {
317 if expanded.error.is_none() {
318 manifest.bins.push(Bin {
319 name: expanded.name.clone(),
320 path: project.source_dir.join(&expanded.test),
321 });
322 }
323 }
324
325 Ok(manifest)
326}
327
328fn make_config() -> Config {
329 Config {
330 build: Build {
331 rustflags: rustflags::make_vec(),
332 },
333 }
334}
335
336#[derive(Debug)]
337enum ExpansionOutcome {
338 Same,
339 Different(Vec<u8>, Vec<u8>),
340 Update,
341 ExpandError(Vec<u8>),
342 NoExpandedFileFound,
343}
344
345struct ExpandedTest {
346 name: Name,
347 test: PathBuf,
348 error: Option<Error>,
349}
350
351impl ExpandedTest {
352 pub fn run<I, S>(
353 &self,
354 project: &Project,
355 expansion_behavior: ExpansionBehavior,
356 args: &Option<I>,
357 ) -> Result<ExpansionOutcome>
358 where
359 I: IntoIterator<Item = S> + Clone,
360 S: AsRef<OsStr>,
361 {
362 let (success, output_bytes) = cargo::expand(project, &self.name, args)?;
363
364 if !success {
365 return Ok(ExpansionOutcome::ExpandError(output_bytes));
366 }
367
368 let file_stem = self
369 .test
370 .file_stem()
371 .expect("no file stem")
372 .to_string_lossy()
373 .into_owned();
374 let mut expanded = self.test.clone();
375 expanded.pop();
376 let expanded = &expanded.join(format!("{}.{}", file_stem, EXPANDED_RS_SUFFIX));
377
378 let output = normalize_expansion(&output_bytes);
379
380 if !expanded.exists() {
381 if let ExpansionBehavior::ExpectFiles = expansion_behavior {
382 return Ok(ExpansionOutcome::NoExpandedFileFound);
383 }
384
385 std::fs::write(expanded, output)?;
387
388 return Ok(ExpansionOutcome::Update);
389 }
390
391 let expected_expansion_bytes = std::fs::read(expanded)?;
392 let expected_expansion = String::from_utf8_lossy(&expected_expansion_bytes);
393
394 let same = output.lines().eq(expected_expansion.lines());
395
396 if !same && project.overwrite {
397 if let ExpansionBehavior::ExpectFiles = expansion_behavior {
398 return Ok(ExpansionOutcome::NoExpandedFileFound);
399 }
400
401 std::fs::write(expanded, output)?;
403
404 return Ok(ExpansionOutcome::Update);
405 }
406
407 Ok(if same {
408 ExpansionOutcome::Same
409 } else {
410 let output_bytes = output.into_bytes(); ExpansionOutcome::Different(expected_expansion_bytes, output_bytes)
412 })
413 }
414}
415
416fn normalize_expansion(input: &[u8]) -> String {
417 let code = String::from_utf8_lossy(input);
418 let mut syntax_tree = match syn::parse_file(&code) {
419 Ok(syntax_tree) => syntax_tree,
420 Err(_) => return code.into_owned(),
421 };
422
423 syntax_tree.attrs.retain(|attr| {
428 if let Meta::List(meta) = &attr.meta {
429 if meta.path.is_ident("feature") {
430 if let Ok(list) =
431 meta.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
432 {
433 if list.len() == 1 {
434 if let Meta::Path(inner) = &list.first().unwrap() {
435 if inner.is_ident("prelude_import") {
436 return false;
437 }
438 }
439 }
440 }
441 }
442 }
443 true
444 });
445
446 syntax_tree.items.retain(|item| {
455 if let Item::Use(item) = item {
456 if let Some(attr) = item.attrs.first() {
457 if attr.path().is_ident("prelude_import") && attr.meta.require_path_only().is_ok() {
458 return false;
459 }
460 }
461 }
462 if let Item::ExternCrate(item) = item {
463 if item.ident == "std" {
464 return false;
465 }
466 }
467 true
468 });
469
470 prettyplease::unparse(&syntax_tree)
471}
472
473fn expand_globs(path: impl AsRef<Path>) -> Vec<ExpandedTest> {
474 fn glob(pattern: &str) -> Result<Vec<PathBuf>> {
475 let mut paths = glob::glob(pattern)?
476 .map(|entry| entry.map_err(Error::from))
477 .collect::<Result<Vec<PathBuf>>>()?;
478 paths.sort();
479 Ok(paths)
480 }
481
482 fn bin_name(i: usize) -> Name {
483 Name(format!("macrotest{:03}", i))
484 }
485
486 let mut vec = Vec::new();
487
488 let name = path
489 .as_ref()
490 .file_stem()
491 .expect("no file stem")
492 .to_string_lossy()
493 .to_string();
494 let mut expanded = ExpandedTest {
495 name: Name(name),
496 test: path.as_ref().to_path_buf(),
497 error: None,
498 };
499
500 if let Some(utf8) = path.as_ref().to_str() {
501 if utf8.contains('*') {
502 match glob(utf8) {
503 Ok(paths) => {
504 for path in paths {
505 vec.push(ExpandedTest {
506 name: bin_name(vec.len()),
507 test: path,
508 error: None,
509 });
510 }
511 }
512 Err(error) => expanded.error = Some(error),
513 }
514 } else {
515 vec.push(expanded);
516 }
517 }
518
519 vec
520}