macrotest-1.0.13/.cargo_vcs_info.json0000644000000001360000000000100131170ustar { "git": { "sha1": "02a5008cd5a1adb969b648b0ab44673700df8a84" }, "path_in_vcs": "" }macrotest-1.0.13/.github/dependabot.yml000064400000000000000000000004441046102023000161010ustar 00000000000000version: 2 updates: - package-ecosystem: cargo directory: "/" schedule: interval: daily time: "21:00" open-pull-requests-limit: 10 - package-ecosystem: cargo directory: "/test-procmacro-project" schedule: interval: daily time: "21:00" open-pull-requests-limit: 10 macrotest-1.0.13/.github/workflows/ci.yml000064400000000000000000000015211046102023000164210ustar 00000000000000name: CI on: push: pull_request: jobs: test: name: Rust ${{matrix.rust}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: fail-fast: false matrix: rust: [nightly, beta, stable] os: [ubuntu, macos, windows] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - uses: taiki-e/cache-cargo-install-action@v1 with: tool: cargo-expand - run: cargo test --manifest-path test-project/Cargo.toml -- --nocapture - run: cargo test --manifest-path test-procmacro-project/Cargo.toml -- --nocapture msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@cargo-hack - run: cargo hack check --rust-version macrotest-1.0.13/.gitignore000064400000000000000000000000621046102023000136750ustar 00000000000000**/target **/*.rs.bk Cargo.lock *.iml *.ipr *.iws macrotest-1.0.13/Cargo.toml0000644000000021060000000000100111140ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.56" name = "macrotest" version = "1.0.13" authors = ["eupn "] description = "Test harness for macro expansion" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/eupn/macrotest" [dependencies.basic-toml] version = "0.1" [dependencies.diff] version = "0.1" [dependencies.glob] version = "0.3" [dependencies.prettyplease] version = "0.2" [dependencies.serde] version = "1.0.105" [dependencies.serde_derive] version = "1.0.105" [dependencies.serde_json] version = "1.0" [dependencies.syn] version = "2" features = ["full"] macrotest-1.0.13/Cargo.toml.orig000064400000000000000000000007611046102023000146020ustar 00000000000000[package] name = "macrotest" version = "1.0.13" # remember to update in lib.rs authors = ["eupn "] edition = "2018" rust-version = "1.56" license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/eupn/macrotest" description = "Test harness for macro expansion" [dependencies] basic-toml = "0.1" diff = "0.1" glob = "0.3" prettyplease = "0.2" serde = "1.0.105" serde_derive = "1.0.105" serde_json = "1.0" syn = { version = "2", features = ["full"] } macrotest-1.0.13/README.md000064400000000000000000000025721046102023000131740ustar 00000000000000# `macrotest` [![Travis-CI](https://api.travis-ci.com/eupn/macrotest.svg?branch=master)](https://travis-ci.com/eupn/macrotest) [![Crates.io](https://img.shields.io/crates/v/macrotest)](https://crates.io/crates/macrotest) ![MSRV 1.56](https://img.shields.io/badge/MSRV-1.56-orange.svg) [![docs.rs](https://docs.rs/macrotest/badge.svg)](https://docs.rs/macrotest/) [![Crates.io](https://img.shields.io/crates/d/macrotest)](https://crates.io/crates/macrotest) [![Crates.io](https://img.shields.io/crates/l/macrotest)](https://crates.io/crates/macrotest) Similar to [trybuild], but allows you to test how declarative or procedural macros are expanded. *Minimal Supported Rust Version: 1.56* ---- ## Documentation Please refer to the [documentation](https://docs.rs/macrotest). ## Example Install [`cargo expand`]. Add to your crate's Cargo.toml: ```toml [dev-dependencies] macrotest = "1" ``` Under your crate's `tests/` directory, create `tests.rs` file containing the following code: ```rust #[test] pub fn pass() { macrotest::expand("tests/expand/*.rs"); } ``` Populate the `tests/expand/` directory with rust source files. Each source file is a macro expansion test case. See [test-project](test-project) and [test-procmacro-project](test-procmacro-project) for the reference. [trybuild]: https://github.com/dtolnay/trybuild [`cargo expand`]: https://github.com/dtolnay/cargo-expand macrotest-1.0.13/src/cargo.rs000064400000000000000000000057321046102023000141460ustar 00000000000000use std::ffi::OsStr; use std::io::BufRead; use std::path::PathBuf; use std::process::Command; use crate::error::{Error, Result}; use crate::expand::Project; use crate::manifest::Name; use crate::rustflags; use serde_derive::Deserialize; #[derive(Deserialize)] pub struct Metadata { pub target_directory: PathBuf, pub workspace_root: PathBuf, } fn raw_cargo() -> Command { Command::new(option_env!("CARGO").unwrap_or("cargo")) } fn cargo(project: &Project) -> Command { let mut cmd = raw_cargo(); cmd.current_dir(&project.dir); cmd.env( "CARGO_TARGET_DIR", &project.inner_target_dir, ); rustflags::set_env(&mut cmd); cmd } pub(crate) fn metadata() -> Result { let output = raw_cargo() .arg("metadata") .arg("--format-version=1") .output() .map_err(Error::Cargo)?; serde_json::from_slice(&output.stdout).map_err(Error::CargoMetadata) } pub(crate) fn expand( project: &Project, name: &Name, args: &Option, ) -> Result<(bool, Vec)> where I: IntoIterator + Clone, S: AsRef, { let mut cargo = cargo(project); let cargo = cargo .arg("expand") .arg("--bin") .arg(name.as_ref()) .arg("--theme") .arg("none"); if let Some(args) = args { cargo.args(args.clone()); } let cargo_expand = cargo .output() .map_err(|e| Error::CargoExpandExecutionError(e.to_string()))?; if !cargo_expand.status.success() { return Ok((false, cargo_expand.stderr)); } Ok((true, cargo_expand.stdout)) } /// Builds dependencies for macro expansion and pipes `cargo` output to `STDOUT`. /// Tries to expand macros in `main.rs` and intentionally filters the result. /// This function is called before macro expansions to speed them up and /// for dependencies build process to be visible for user. pub(crate) fn build_dependencies(project: &Project) -> Result<()> { use std::io::Write; let stdout = cargo(project) .arg("expand") .arg("--bin") .arg(project.name.clone()) .arg("--theme") .arg("none") .stdout(std::process::Stdio::piped()) .spawn()? .stdout .ok_or(Error::CargoFail)?; let reader = std::io::BufReader::new(stdout); // Filter ignored lines and main.rs content reader .lines() .filter_map(|line| line.ok()) .filter(|line| !line.starts_with("fn main() {}")) .filter(|line| !line_should_be_ignored(line)) .for_each(|line| { let _ = writeln!(std::io::stdout(), "{}", line); }); Ok(()) } const IGNORED_LINES: [&str; 5] = [ "#![feature(prelude_import)]", "#[prelude_import]", "use std::prelude::", "#[macro_use]", "extern crate std;", ]; fn line_should_be_ignored(line: &str) -> bool { for check in IGNORED_LINES.iter() { if line.starts_with(check) { return true; } } false } macrotest-1.0.13/src/dependencies.rs000064400000000000000000000142351046102023000154770ustar 00000000000000use crate::error::Error; use crate::manifest::Edition; use serde::de::value::MapAccessDeserializer; use serde::de::{self, Deserialize, Deserializer, Visitor}; use serde::ser::{Serialize, Serializer}; use serde_derive::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap as Map; use std::fmt; use std::fs; use std::path::Path; use std::path::PathBuf; pub(crate) fn get_manifest(manifest_dir: &Path) -> Manifest { try_get_manifest(manifest_dir).unwrap_or_default() } fn try_get_manifest(manifest_dir: &Path) -> Result { let cargo_toml_path = manifest_dir.join("Cargo.toml"); let manifest_str = fs::read_to_string(cargo_toml_path)?; let mut manifest: Manifest = basic_toml::from_str(&manifest_str)?; fix_dependencies(&mut manifest.dependencies, manifest_dir); fix_dependencies(&mut manifest.dev_dependencies, manifest_dir); Ok(manifest) } pub(crate) fn get_workspace_manifest(manifest_dir: &Path) -> WorkspaceManifest { try_get_workspace_manifest(manifest_dir).unwrap_or_default() } pub(crate) fn try_get_workspace_manifest(manifest_dir: &Path) -> Result { let cargo_toml_path = manifest_dir.join("Cargo.toml"); let manifest_str = fs::read_to_string(cargo_toml_path)?; let mut manifest: WorkspaceManifest = basic_toml::from_str(&manifest_str)?; fix_dependencies(&mut manifest.workspace.dependencies, manifest_dir); fix_patches(&mut manifest.patch, manifest_dir); fix_replacements(&mut manifest.replace, manifest_dir); Ok(manifest) } fn fix_dependencies(dependencies: &mut Map, dir: &Path) { dependencies.remove("macrotest"); for dep in dependencies.values_mut() { dep.path = dep.path.as_ref().map(|path| dir.join(path)); } } fn fix_patches(patches: &mut Map, dir: &Path) { for registry in patches.values_mut() { registry.crates.remove("macrotest"); for patch in registry.crates.values_mut() { patch.path = patch.path.as_ref().map(|path| dir.join(path)); } } } fn fix_replacements(replacements: &mut Map, dir: &Path) { replacements.remove("macrotest"); for replacement in replacements.values_mut() { replacement.path = replacement.path.as_ref().map(|path| dir.join(path)); } } #[derive(Deserialize, Default, Debug)] pub struct WorkspaceManifest { #[serde(default)] pub workspace: Workspace, #[serde(default)] pub patch: Map, #[serde(default)] pub replace: Map, } #[derive(Deserialize, Default, Debug)] pub struct Workspace { #[serde(default)] pub package: WorkspacePackage, #[serde(default)] pub dependencies: Map, } #[derive(Deserialize, Default, Debug)] pub struct WorkspacePackage { pub edition: Option, } #[derive(Deserialize, Default, Debug)] pub struct Manifest { #[serde(default, rename = "cargo-features")] pub cargo_features: Vec, #[serde(default)] pub package: Package, #[serde(default)] pub features: Map>, #[serde(default)] pub dependencies: Map, #[serde(default, alias = "dev-dependencies")] pub dev_dependencies: Map, } #[derive(Deserialize, Default, Debug)] pub struct Package { #[serde(default)] pub edition: Edition, } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(remote = "Self")] pub struct Dependency { #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde( rename = "default-features", default = "get_true", skip_serializing_if = "is_true" )] pub default_features: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub features: Vec, #[serde(default, skip_serializing_if = "is_false")] pub workspace: bool, #[serde(flatten)] pub rest: Map, } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(transparent)] pub struct RegistryPatch { crates: Map, } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Patch { #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub git: Option, #[serde(skip_serializing_if = "Option::is_none")] pub branch: Option, } fn get_true() -> bool { true } #[allow(clippy::trivially_copy_pass_by_ref)] fn is_true(boolean: &bool) -> bool { *boolean } #[allow(clippy::trivially_copy_pass_by_ref)] fn is_false(boolean: &bool) -> bool { !*boolean } impl Serialize for Dependency { fn serialize(&self, serializer: S) -> Result where S: Serializer, { Dependency::serialize(self, serializer) } } impl<'de> Deserialize<'de> for Dependency { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct DependencyVisitor; impl<'de> Visitor<'de> for DependencyVisitor { type Value = Dependency; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str( "a version string like \"0.9.8\" or a \ dependency like { version = \"0.9.8\" }", ) } fn visit_str(self, s: &str) -> Result where E: de::Error, { Ok(Dependency { version: Some(s.to_owned()), path: None, default_features: true, features: Vec::new(), workspace: false, rest: Map::new(), }) } fn visit_map(self, map: M) -> Result where M: de::MapAccess<'de>, { Dependency::deserialize(MapAccessDeserializer::new(map)) } } deserializer.deserialize_any(DependencyVisitor) } } macrotest-1.0.13/src/error.rs000064400000000000000000000035371046102023000142050ustar 00000000000000#[derive(Debug)] pub(crate) enum Error { Cargo(std::io::Error), CargoExpandExecutionError(String), CargoFail, CargoMetadata(serde_json::error::Error), IoError(std::io::Error), TomlError(basic_toml::Error), GlobError(glob::GlobError), GlobPatternError(glob::PatternError), ManifestDirError, PkgName, UnrecognizedEnv(std::ffi::OsString), } pub(crate) type Result = std::result::Result; impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { use self::Error::*; match self { Cargo(e) => write!(f, "{}", e), CargoExpandExecutionError(e) => write!(f, "Failed to execute cargo command: {}", e), CargoFail => write!(f, "cargo reported an error"), CargoMetadata(e) => write!(f, "{}", e), IoError(e) => write!(f, "{}", e), TomlError(e) => write!(f, "{}", e), GlobError(e) => write!(f, "{}", e), GlobPatternError(e) => write!(f, "{}", e), ManifestDirError => write!(f, "could not find CARGO_MANIFEST_DIR env var"), PkgName => write!(f, "could not find CARGO_PKG_NAME env var"), UnrecognizedEnv(e) => write!( f, "unrecognized value of MACROTEST: \"{}\"", e.to_string_lossy() ), } } } impl From for Error { fn from(e: std::io::Error) -> Self { Error::IoError(e) } } impl From for Error { fn from(e: basic_toml::Error) -> Self { Error::TomlError(e) } } impl From for Error { fn from(e: glob::GlobError) -> Self { Error::GlobError(e) } } impl From for Error { fn from(e: glob::PatternError) -> Self { Error::GlobPatternError(e) } } macrotest-1.0.13/src/expand.rs000064400000000000000000000363201046102023000143270ustar 00000000000000use std::env; use std::ffi::OsStr; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; use crate::cargo; use crate::dependencies::{self, Dependency}; use crate::features; use crate::manifest::{Bin, Build, Config, Manifest, Name, Package, Workspace}; use crate::message::{message_different, message_expansion_error}; use crate::rustflags; use crate::{error::Error, error::Result}; use syn::punctuated::Punctuated; use syn::{Item, Meta, Token}; /// An extension for files containing `cargo expand` result. const EXPANDED_RS_SUFFIX: &str = "expanded.rs"; #[derive(Debug)] pub(crate) struct Project { pub dir: PathBuf, source_dir: PathBuf, /// Used for the inner runs of cargo() pub inner_target_dir: PathBuf, pub name: String, pub features: Option>, workspace: PathBuf, overwrite: bool, } /// This `Drop` implementation will clean up the temporary crates when expansion is finished. /// This is to prevent pollution of the filesystem with dormant files. impl Drop for Project { fn drop(&mut self) { if let Err(e) = fs::remove_dir_all(&self.dir) { eprintln!( "Failed to cleanup the directory `{}`: {}", self.dir.to_string_lossy(), e ); } } } /// Attempts to expand macros in files that match glob pattern. /// /// # Refresh behavior /// /// If no matching `.expanded.rs` files present, they will be created and result of expansion /// will be written into them. /// /// # Panics /// /// Will panic if matching `.expanded.rs` file is present, but has different expanded code in it. pub fn expand(path: impl AsRef) { run_tests( path, ExpansionBehavior::RegenerateFiles, Option::>::None, ); } /// Same as [`expand`] but allows to pass additional arguments to `cargo-expand`. /// /// [`expand`]: expand/fn.expand.html pub fn expand_args(path: impl AsRef, args: I) where I: IntoIterator + Clone, S: AsRef, { run_tests(path, ExpansionBehavior::RegenerateFiles, Some(args)); } /// Attempts to expand macros in files that match glob pattern. /// More strict version of [`expand`] function. /// /// # Refresh behavior /// /// If no matching `.expanded.rs` files present, it considered a failed test. /// /// # Panics /// /// Will panic if no matching `.expanded.rs` file is present. Otherwise it will exhibit the same /// behavior as in [`expand`]. /// /// [`expand`]: expand/fn.expand.html pub fn expand_without_refresh(path: impl AsRef) { run_tests( path, ExpansionBehavior::ExpectFiles, Option::>::None, ); } /// Same as [`expand_without_refresh`] but allows to pass additional arguments to `cargo-expand`. /// /// [`expand_without_refresh`]: expand/fn.expand_without_refresh.html pub fn expand_without_refresh_args(path: impl AsRef, args: I) where I: IntoIterator + Clone, S: AsRef, { run_tests(path, ExpansionBehavior::ExpectFiles, Some(args)); } #[derive(Debug, Copy, Clone)] enum ExpansionBehavior { RegenerateFiles, ExpectFiles, } fn run_tests(path: impl AsRef, expansion_behavior: ExpansionBehavior, args: Option) where I: IntoIterator + Clone, S: AsRef, { let tests = expand_globs(&path) .into_iter() .filter(|t| !t.test.to_string_lossy().ends_with(EXPANDED_RS_SUFFIX)) .collect::>(); let len = tests.len(); println!("Running {} macro expansion tests", len); let project = prepare(&tests).unwrap_or_else(|err| { panic!("prepare failed: {:#?}", err); }); let mut failures = 0; for test in tests { let path = test.test.display(); let expanded_path = test.test.with_extension(EXPANDED_RS_SUFFIX); match test.run(&project, expansion_behavior, &args) { Ok(outcome) => match outcome { ExpansionOutcome::Same => { let _ = writeln!(std::io::stdout(), "{} - ok", path); } ExpansionOutcome::Different(a, b) => { message_different(&path.to_string(), &a, &b); failures += 1; } ExpansionOutcome::Update => { let _ = writeln!(std::io::stderr(), "{} - refreshed", expanded_path.display()); } ExpansionOutcome::ExpandError(msg) => { message_expansion_error(msg); failures += 1; } ExpansionOutcome::NoExpandedFileFound => { let _ = writeln!( std::io::stderr(), "{} is expected but not found", expanded_path.display() ); failures += 1; } }, Err(e) => { eprintln!("Error: {:#?}", e); failures += 1; } } } if failures > 0 { eprintln!("\n\n"); panic!("{} of {} tests failed", failures, len); } } fn prepare(tests: &[ExpandedTest]) -> Result { let metadata = cargo::metadata()?; let target_dir = metadata.target_directory; let workspace = metadata.workspace_root; let crate_name = env::var("CARGO_PKG_NAME").map_err(|_| Error::PkgName)?; let source_dir = env::var_os("CARGO_MANIFEST_DIR") .map(PathBuf::from) .ok_or(Error::ManifestDirError)?; let features = features::find(); let overwrite = match env::var_os("MACROTEST") { Some(ref v) if v == "overwrite" => true, Some(v) => return Err(Error::UnrecognizedEnv(v)), None => false, }; static COUNT: AtomicUsize = AtomicUsize::new(0); // Use unique string for the crate dir to // prevent conflicts when running parallel tests. let unique_string: String = format!("macrotest{:03}", COUNT.fetch_add(1, Ordering::SeqCst)); let dir = path!(target_dir / "tests" / crate_name / unique_string); if dir.exists() { // Remove remaining artifacts from previous runs if exist. // For example, if the user stops the test with Ctrl-C during a previous // run, the destructor of Project will not be called. fs::remove_dir_all(&dir)?; } let inner_target_dir = path!(target_dir / "tests" / "macrotest"); let mut project = Project { dir, source_dir, inner_target_dir, name: format!("{}-tests", crate_name), features, workspace, overwrite, }; let manifest = make_manifest(crate_name, &project, tests)?; let manifest_toml = basic_toml::to_string(&manifest)?; let config = make_config(); let config_toml = basic_toml::to_string(&config)?; if let Some(enabled_features) = &mut project.features { enabled_features.retain(|feature| manifest.features.contains_key(feature)); } fs::create_dir_all(path!(project.dir / ".cargo"))?; fs::write(path!(project.dir / ".cargo" / "config.toml"), config_toml)?; fs::write(path!(project.dir / "Cargo.toml"), manifest_toml)?; fs::write(path!(project.dir / "main.rs"), b"fn main() {}\n")?; let source_lockfile = path!(project.workspace / "Cargo.lock"); match fs::copy(source_lockfile, path!(project.dir / "Cargo.lock")) { Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0), otherwise => otherwise, }?; fs::create_dir_all(&project.inner_target_dir)?; cargo::build_dependencies(&project)?; Ok(project) } fn make_manifest( crate_name: String, project: &Project, tests: &[ExpandedTest], ) -> Result { let source_manifest = dependencies::get_manifest(&project.source_dir); let workspace_manifest = dependencies::get_workspace_manifest(&project.workspace); let features = source_manifest .features .iter() .map(|(feature, source_deps)| { let enable = format!("{}/{}", crate_name, feature); let mut deps = vec![enable]; deps.extend( source_deps .iter() .filter(|dep| dep.starts_with("dep:")) .cloned(), ); (feature.clone(), deps) }) .collect(); let mut manifest = Manifest { cargo_features: source_manifest.cargo_features.clone(), package: Package { name: project.name.clone(), version: "0.0.0".to_owned(), edition: source_manifest.package.edition, publish: false, }, features, dependencies: std::collections::BTreeMap::new(), bins: Vec::new(), workspace: Some(Workspace { package: crate::manifest::WorkspacePackage { edition: workspace_manifest.workspace.package.edition, }, dependencies: workspace_manifest.workspace.dependencies, }), // Within a workspace, only the [patch] and [replace] sections in // the workspace root's Cargo.toml are applied by Cargo. patch: workspace_manifest.patch, replace: workspace_manifest.replace, }; manifest.dependencies.extend(source_manifest.dependencies); manifest .dependencies .extend(source_manifest.dev_dependencies); manifest.dependencies.insert( crate_name, Dependency { version: None, path: Some(project.source_dir.clone()), default_features: false, features: Vec::new(), workspace: false, rest: std::collections::BTreeMap::new(), }, ); manifest.bins.push(Bin { name: Name(project.name.to_owned()), path: Path::new("main.rs").to_owned(), }); for expanded in tests { if expanded.error.is_none() { manifest.bins.push(Bin { name: expanded.name.clone(), path: project.source_dir.join(&expanded.test), }); } } Ok(manifest) } fn make_config() -> Config { Config { build: Build { rustflags: rustflags::make_vec(), }, } } #[derive(Debug)] enum ExpansionOutcome { Same, Different(Vec, Vec), Update, ExpandError(Vec), NoExpandedFileFound, } struct ExpandedTest { name: Name, test: PathBuf, error: Option, } impl ExpandedTest { pub fn run( &self, project: &Project, expansion_behavior: ExpansionBehavior, args: &Option, ) -> Result where I: IntoIterator + Clone, S: AsRef, { let (success, output_bytes) = cargo::expand(project, &self.name, args)?; if !success { return Ok(ExpansionOutcome::ExpandError(output_bytes)); } let file_stem = self .test .file_stem() .expect("no file stem") .to_string_lossy() .into_owned(); let mut expanded = self.test.clone(); expanded.pop(); let expanded = &expanded.join(format!("{}.{}", file_stem, EXPANDED_RS_SUFFIX)); let output = normalize_expansion(&output_bytes); if !expanded.exists() { if let ExpansionBehavior::ExpectFiles = expansion_behavior { return Ok(ExpansionOutcome::NoExpandedFileFound); } // Write a .expanded.rs file contents std::fs::write(expanded, output)?; return Ok(ExpansionOutcome::Update); } let expected_expansion_bytes = std::fs::read(expanded)?; let expected_expansion = String::from_utf8_lossy(&expected_expansion_bytes); let same = output.lines().eq(expected_expansion.lines()); if !same && project.overwrite { if let ExpansionBehavior::ExpectFiles = expansion_behavior { return Ok(ExpansionOutcome::NoExpandedFileFound); } // Write a .expanded.rs file contents std::fs::write(expanded, output)?; return Ok(ExpansionOutcome::Update); } Ok(if same { ExpansionOutcome::Same } else { let output_bytes = output.into_bytes(); // Use normalized text for a message ExpansionOutcome::Different(expected_expansion_bytes, output_bytes) }) } } fn normalize_expansion(input: &[u8]) -> String { let code = String::from_utf8_lossy(input); let mut syntax_tree = match syn::parse_file(&code) { Ok(syntax_tree) => syntax_tree, Err(_) => return code.into_owned(), }; // Strip the following: // // #![feature(prelude_import)] // syntax_tree.attrs.retain(|attr| { if let Meta::List(meta) = &attr.meta { if meta.path.is_ident("feature") { if let Ok(list) = meta.parse_args_with(Punctuated::::parse_terminated) { if list.len() == 1 { if let Meta::Path(inner) = &list.first().unwrap() { if inner.is_ident("prelude_import") { return false; } } } } } } true }); // Strip the following: // // #[prelude_import] // use std::prelude::$edition::*; // // #[macro_use] // extern crate std; // syntax_tree.items.retain(|item| { if let Item::Use(item) = item { if let Some(attr) = item.attrs.first() { if attr.path().is_ident("prelude_import") && attr.meta.require_path_only().is_ok() { return false; } } } if let Item::ExternCrate(item) = item { if item.ident == "std" { return false; } } true }); prettyplease::unparse(&syntax_tree) } fn expand_globs(path: impl AsRef) -> Vec { fn glob(pattern: &str) -> Result> { let mut paths = glob::glob(pattern)? .map(|entry| entry.map_err(Error::from)) .collect::>>()?; paths.sort(); Ok(paths) } fn bin_name(i: usize) -> Name { Name(format!("macrotest{:03}", i)) } let mut vec = Vec::new(); let name = path .as_ref() .file_stem() .expect("no file stem") .to_string_lossy() .to_string(); let mut expanded = ExpandedTest { name: Name(name), test: path.as_ref().to_path_buf(), error: None, }; if let Some(utf8) = path.as_ref().to_str() { if utf8.contains('*') { match glob(utf8) { Ok(paths) => { for path in paths { vec.push(ExpandedTest { name: bin_name(vec.len()), test: path, error: None, }); } } Err(error) => expanded.error = Some(error), } } else { vec.push(expanded); } } vec } macrotest-1.0.13/src/features.rs000064400000000000000000000053171046102023000146700ustar 00000000000000use serde::de::{self, Deserialize, DeserializeOwned, Deserializer}; use serde_derive::Deserialize; use std::env; use std::error::Error; use std::ffi::OsStr; use std::fs; use std::path::PathBuf; pub fn find() -> Option> { try_find().ok() } struct Ignored; impl From for Ignored { fn from(_error: E) -> Self { Ignored } } #[derive(Deserialize)] struct Build { #[serde(deserialize_with = "from_json")] features: Vec, } fn try_find() -> Result, Ignored> { // This will look something like: // /path/to/crate_name/target/debug/deps/test_name-HASH let test_binary = env::args_os().next().ok_or(Ignored)?; // The hash at the end is ascii so not lossy, rest of conversion doesn't // matter. let test_binary_lossy = test_binary.to_string_lossy(); let hash = test_binary_lossy .get(test_binary_lossy.len() - 17..) .ok_or(Ignored)?; if !hash.starts_with('-') || !hash[1..].bytes().all(is_lower_hex_digit) { return Err(Ignored); } let binary_path = PathBuf::from(&test_binary); // Feature selection is saved in: // /path/to/crate_name/target/debug/.fingerprint/*-HASH/*-HASH.json let up = binary_path .parent() .ok_or(Ignored)? .parent() .ok_or(Ignored)?; let fingerprint_dir = up.join(".fingerprint"); if !fingerprint_dir.is_dir() { return Err(Ignored); } let mut hash_matches = Vec::new(); for entry in fingerprint_dir.read_dir()? { let entry = entry?; let is_dir = entry.file_type()?.is_dir(); let matching_hash = entry.file_name().to_string_lossy().ends_with(hash); if is_dir && matching_hash { hash_matches.push(entry.path()); } } if hash_matches.len() != 1 { return Err(Ignored); } let mut json_matches = Vec::new(); for entry in hash_matches[0].read_dir()? { let entry = entry?; let is_file = entry.file_type()?.is_file(); let is_json = entry.path().extension() == Some(OsStr::new("json")); if is_file && is_json { json_matches.push(entry.path()); } } if json_matches.len() != 1 { return Err(Ignored); } let build_json = fs::read_to_string(&json_matches[0])?; let build: Build = serde_json::from_str(&build_json)?; Ok(build.features) } fn is_lower_hex_digit(byte: u8) -> bool { byte >= b'0' && byte <= b'9' || byte >= b'a' && byte <= b'f' } fn from_json<'de, T, D>(deserializer: D) -> Result where T: DeserializeOwned, D: Deserializer<'de>, { let json = String::deserialize(deserializer)?; serde_json::from_str(&json).map_err(de::Error::custom) } macrotest-1.0.13/src/lib.rs000064400000000000000000000123431046102023000136150ustar 00000000000000#![crate_type = "lib"] #![doc(html_root_url = "https://docs.rs/macrotest/1.0.13")] //! ####   Test harness for macro expansion. //! //! Similar to [trybuild], but allows you to write tests on how macros are expanded. //! //! *Minimal Supported Rust Version: 1.56* //! //!
//! //! # Macro expansion tests //! //! A minimal `macrotest` setup looks like this: //! //! ```rust //! #[test] //! pub fn pass() { //! macrotest::expand("tests/expand/*.rs"); //! // Alternatively, //! macrotest::expand_without_refresh("tests/expand/*.rs"); //! } //! ``` //! //! The test can be run with `cargo test`. This test will invoke the [`cargo expand`] command //! on each of the source files that matches the glob pattern and will compare the expansion result //! with the corresponding `*.expanded.rs` file. //! //! If a `*.expanded.rs` file doesn't exists and it's not explicitly expected to (see [`expand_without_refresh`]), //! it will be created (this is how you update your tests). //! //! Possible test outcomes are: //! - **Pass**: expansion succeeded and the result is the same as in the `.expanded.rs` file //! - **Fail**: expansion was different from the `.expanded.rs` file content //! - **Refresh**: `.expanded.rs` didn't exist and has been created //! - **Refresh-fail**: `.expanded.rs` is expected to be present, but not exists. See [`expand_without_refresh`]. //! //! *Note:* when working with multiple expansion test files, it is recommended to //! specify wildcard (*.rs) instead of doing a multiple calls to `expand` functions for individual files. //! Usage of wildcards for multiple files will group them under a single temporary crate for which //! dependencies will be built a single time. In contrast, calling `expand` functions for each //! source file will create multiple temporary crates and that will reduce performance as depdendencies //! will be build for each of the temporary crates. //! //! ## Passing additional arguments to `cargo expand` //! //! It's possible to specify additional arguments for [`cargo expand`] command. //! //! In order to do so, use the following functions with `_args` suffix: //! - [`expand_args`] //! - [`expand_without_refresh_args`] //! //! Example: //! //! ```rust //! pub fn pass() { //! macrotest::expand_args("tests/expand/*.rs", &["--features", "my-feature"]); //! // Or //! macrotest::expand_without_refresh_args("tests/expand/*.rs", &["--features", "my-feature"]); //! } //! ``` //! //! The `_args` functions will result in the following [`cargo expand`] command being run: //! //! ```bash //! cargo expand --bin --theme none --features my-feature //! ``` //! //! # Workflow //! //! First of all, the [`cargo expand`] tool must be present. You can install it via cargo: //! //! ```bash //! cargo install --locked cargo-expand //! ``` //! //! (In CI, you'll want to pin to a particular version, //! since //! [cargo expand's output is not stable across versions](https://github.com/dtolnay/cargo-expand/issues/179). //! Look up the //! [current version](https://crates.io/crates/cargo-expand) //! and do something like `cargo install --locked --version 1.0.81 cargo-expand`.) //! //! ## Setting up a test project //! //! In your crate that provides procedural or declarative macros, under the `tests` directory, //! create an `expand` directory and populate it with different expansion test cases as //! rust source files. //! //! Then create a `tests.rs` file that will run the tests: //! //! ```rust //! #[test] //! pub fn pass() { //! macrotest::expand("tests/expand/*.rs"); //! // Or: //! macrotest::expand_without_refresh("tests/expand/*.rs"); //! } //! ``` //! //! And then you can run `cargo test`, which will //! //! 1. Expand macros in source files that match glob pattern //! 1. In case if [`expand`] function is used: //! - On the first run, generate the `*.expanded.rs` files for each of the test cases under //! the `expand` directory //! - On subsequent runs, compare test cases' expansion result with the //! content of the respective `*.expanded.rs` files //! 1. In case if [`expand_without_refresh`] is used: //! - On each run, it will compare test cases' expansion result with the content of the //! respective `*.expanded.rs` files. //! - If one or more `*.expanded.rs` files is not found, the test will fail. //! //! ## Updating `.expanded.rs` //! //! This applicable only to tests that are using [`expand`] or [`expand_args`] function. //! //! Run tests with the environment variable `MACROTEST=overwrite` or remove the `*.expanded.rs` //! files and re-run the corresponding tests. Files will be created automatically; hand-writing //! them is not recommended. //! //! [`expand_without_refresh`]: expand/fn.expand_without_refresh.html //! [`expand_without_refresh_args`]: expand/fn.expand_without_refresh_args.html //! [`expand`]: expand/fn.expand.html //! [`expand_args`]: expand/fn.expand_args.html //! [trybuild]: https://github.com/dtolnay/trybuild //! [`cargo expand`]: https://github.com/dtolnay/cargo-expand #[macro_use] mod path; mod cargo; mod dependencies; mod error; mod expand; mod features; mod manifest; mod message; mod rustflags; pub use expand::expand; pub use expand::expand_args; pub use expand::expand_without_refresh; pub use expand::expand_without_refresh_args; macrotest-1.0.13/src/manifest.rs000064400000000000000000000041631046102023000146560ustar 00000000000000use crate::dependencies::{Dependency, Patch, RegistryPatch}; use serde_derive::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap as Map; use std::ffi::OsStr; use std::path::PathBuf; #[derive(Serialize, Debug)] pub struct Manifest { #[serde(rename = "cargo-features")] #[serde(skip_serializing_if = "Vec::is_empty")] pub cargo_features: Vec, pub package: Package, #[serde(skip_serializing_if = "Map::is_empty")] pub features: Map>, pub dependencies: Map, #[serde(rename = "bin")] pub bins: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub workspace: Option, #[serde(skip_serializing_if = "Map::is_empty")] pub patch: Map, #[serde(skip_serializing_if = "Map::is_empty")] pub replace: Map, } #[derive(Serialize, Debug)] pub struct Package { pub name: String, pub version: String, pub publish: bool, pub edition: Edition, } // Do not use enum for edition for future-compatibility. #[derive(Serialize, Deserialize, Debug)] pub struct Edition(pub Value); #[derive(Serialize, Debug)] pub struct Bin { pub name: Name, pub path: PathBuf, } #[derive(Serialize, Clone, Debug)] pub struct Name(pub String); #[derive(Serialize, Debug)] pub struct Config { pub build: Build, } #[derive(Serialize, Debug)] pub struct Build { pub rustflags: Vec, } #[derive(Serialize, Debug)] pub struct Workspace { #[serde(skip_serializing_if = "WorkspacePackage::is_none")] pub package: WorkspacePackage, #[serde(skip_serializing_if = "Map::is_empty")] pub dependencies: Map, } #[derive(Serialize, Debug)] pub struct WorkspacePackage { #[serde(skip_serializing_if = "Option::is_none")] pub edition: Option, } impl WorkspacePackage { fn is_none(&self) -> bool { self.edition.is_none() } } impl Default for Edition { fn default() -> Self { Self("2021".into()) } } impl AsRef for Name { fn as_ref(&self) -> &OsStr { self.0.as_ref() } } macrotest-1.0.13/src/message.rs000064400000000000000000000033621046102023000144740ustar 00000000000000use diff::Result; /// Prints the difference of the two snippets of expanded code. pub(crate) fn message_different(name: &str, a: &[u8], b: &[u8]) { let a = String::from_utf8_lossy(&a); let b = String::from_utf8_lossy(&b); let changes = diff::lines(&a, &b); let mut lines_added = 0; let mut lines_removed = 0; for diff in &changes { match diff { Result::Left(_) => lines_added += 1, Result::Right(_) => lines_removed += 1, _ => (), } } eprintln!("{} - different!", name); eprintln!( "Diff [lines: {} added, {} removed]:", lines_added, lines_removed ); eprintln!("--------------------------"); for change in changes { match change { Result::Both(x, _) => { eprintln!(" {}", x); } Result::Left(x) => { eprintln!("+{}", x); } Result::Right(x) => { eprintln!("-{}", x); } } } eprintln!("--------------------------"); } /// Prints an error from `cargo expand` invocation. /// Makes some suggestions when possible. pub(crate) fn message_expansion_error(msg: Vec) { let msg = String::from_utf8(msg); eprintln!("Expansion error:"); if let Ok(msg) = msg { eprintln!("{}", msg); // No `cargo expand` subcommand installed, make a suggestion if msg.contains("no such subcommand: `expand`") { eprintln!("Perhaps, `cargo expand` is not installed?"); eprintln!("Install it by running:"); eprintln!(); eprintln!("\tcargo install cargo-expand"); eprintln!(); } } else { eprintln!(""); } } macrotest-1.0.13/src/path.rs000064400000000000000000000021331046102023000137770ustar 00000000000000// credits: dtolnay macro_rules! path { ($($tt:tt)+) => { tokenize_path!([] [] $($tt)+) }; } // Private implementation detail. macro_rules! tokenize_path { ([$(($($component:tt)+))*] [$($cur:tt)+] / $($rest:tt)+) => { tokenize_path!([$(($($component)+))* ($($cur)+)] [] $($rest)+) }; ([$(($($component:tt)+))*] [$($cur:tt)*] $first:tt $($rest:tt)*) => { tokenize_path!([$(($($component)+))*] [$($cur)* $first] $($rest)*) }; ([$(($($component:tt)+))*] [$($cur:tt)+]) => { tokenize_path!([$(($($component)+))* ($($cur)+)]) }; ([$(($($component:tt)+))*]) => {{ let mut path = std::path::PathBuf::new(); $( path.push(&($($component)+)); )* path }}; } #[test] fn test_path_macro() { use std::path::{Path, PathBuf}; struct Project { dir: PathBuf, } let project = Project { dir: PathBuf::from("../target/tests"), }; let cargo_dir = path!(project.dir / ".cargo" / "config.toml"); assert_eq!(cargo_dir, Path::new("../target/tests/.cargo/config.toml")); } macrotest-1.0.13/src/rustflags.rs000064400000000000000000000011671046102023000150630ustar 00000000000000use std::env; use std::process::Command; const RUSTFLAGS: &str = "RUSTFLAGS"; const IGNORED_LINTS: &[&str] = &["dead_code"]; pub fn make_vec() -> Vec { let mut rustflags = Vec::new(); for &lint in IGNORED_LINTS { rustflags.push("-A".to_owned()); rustflags.push(lint.to_owned()); } rustflags } pub fn set_env(cmd: &mut Command) { let mut rustflags = match env::var_os(RUSTFLAGS) { Some(rustflags) => rustflags, None => return, }; for flag in make_vec() { rustflags.push(" "); rustflags.push(flag); } cmd.env(RUSTFLAGS, rustflags); }