├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── src ├── cargo.rs ├── dependencies.rs ├── error.rs ├── expand.rs ├── features.rs ├── lib.rs ├── manifest.rs ├── message.rs ├── path.rs └── rustflags.rs ├── test-procmacro-project ├── .gitignore ├── Cargo.toml ├── src │ └── lib.rs └── tests │ ├── expand │ ├── attribute.expanded.rs │ ├── attribute.rs │ ├── derive.expanded.rs │ ├── derive.rs │ ├── macro.expanded.rs │ └── macro.rs │ └── tests.rs └── test-project ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src └── lib.rs └── tests ├── expand ├── first.expanded.rs ├── first.rs ├── fourth.expanded.rs ├── fourth.rs ├── second.expanded.rs ├── second.rs ├── third.expanded.rs └── third.rs ├── expand_args ├── with_args.expanded.rs └── with_args.rs ├── no_expanded ├── first.rs ├── fourth.rs ├── second.rs └── third.rs ├── no_expanded_args └── with_args.rs ├── par_tests_regression.rs ├── pr61 ├── a │ ├── test.expanded.rs │ └── test.rs └── b │ ├── test.expanded.rs │ └── test.rs └── tests.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: cargo 10 | directory: "/test-procmacro-project" 11 | schedule: 12 | interval: daily 13 | time: "21:00" 14 | open-pull-requests-limit: 10 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: Rust ${{matrix.rust}} on ${{matrix.os}} 10 | runs-on: ${{matrix.os}}-latest 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | rust: [nightly, beta, stable] 16 | os: [ubuntu, macos, windows] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@master 21 | with: 22 | toolchain: ${{ matrix.rust }} 23 | 24 | - uses: taiki-e/cache-cargo-install-action@v1 25 | with: 26 | tool: cargo-expand 27 | 28 | - run: cargo test --manifest-path test-project/Cargo.toml -- --nocapture 29 | 30 | - run: cargo test --manifest-path test-procmacro-project/Cargo.toml -- --nocapture 31 | 32 | msrv: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: taiki-e/install-action@cargo-hack 37 | - run: cargo hack check --rust-version 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/*.rs.bk 3 | Cargo.lock 4 | *.iml 5 | *.ipr 6 | *.iws 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macrotest" 3 | version = "1.1.0" 4 | authors = ["eupn "] 5 | edition = "2018" 6 | rust-version = "1.65" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/eupn/macrotest" 10 | description = "Test harness for macro expansion" 11 | 12 | [dependencies] 13 | diff = "0.1" 14 | fastrand = "2" 15 | glob = "0.3" 16 | prettyplease = "0.2" 17 | serde = "1.0.105" 18 | serde_derive = "1.0.105" 19 | serde_json = "1.0" 20 | syn = { version = "2", features = ["full"] } 21 | toml_edit = { version = "0.22", features = ["serde"] } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `macrotest` 2 | 3 | [![Github Actions](https://img.shields.io/github/actions/workflow/status/eupn/macrotest/ci.yml?branch=master)](https://github.com/eupn/macrotest/actions) 4 | [![Crates.io](https://img.shields.io/crates/v/macrotest)](https://crates.io/crates/macrotest) 5 | [![Crates.io (MSRV)](https://img.shields.io/crates/msrv/macrotest)](https://crates.io/crates/macrotest) 6 | [![docs.rs](https://docs.rs/macrotest/badge.svg)](https://docs.rs/macrotest/) 7 | [![Crates.io (Downloads)](https://img.shields.io/crates/d/macrotest)](https://crates.io/crates/macrotest) 8 | [![Crates.io (License)](https://img.shields.io/crates/l/macrotest)](https://crates.io/crates/macrotest) 9 | 10 | Similar to [trybuild], but allows you to test how declarative or procedural macros are expanded. 11 | 12 | *Minimal Supported Rust Version: 1.65* 13 | 14 | ---- 15 | 16 | ## Documentation 17 | 18 | Please refer to the [documentation](https://docs.rs/macrotest). 19 | 20 | ## Example 21 | 22 | Install [`cargo expand`]. 23 | 24 | Add to your crate's Cargo.toml: 25 | 26 | ```toml 27 | [dev-dependencies] 28 | macrotest = "1" 29 | ``` 30 | 31 | Under your crate's `tests/` directory, create `tests.rs` file containing the following code: 32 | 33 | ```rust 34 | #[test] 35 | pub fn pass() { 36 | macrotest::expand("tests/expand/*.rs"); 37 | } 38 | ``` 39 | 40 | Populate the `tests/expand/` directory with rust source files. Each source file is a macro expansion test case. 41 | 42 | See [test-project](test-project) and [test-procmacro-project](test-procmacro-project) for the reference. 43 | 44 | [trybuild]: https://github.com/dtolnay/trybuild 45 | [`cargo expand`]: https://github.com/dtolnay/cargo-expand 46 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::io::BufRead; 3 | use std::path::PathBuf; 4 | use std::process::Command; 5 | 6 | use crate::error::{Error, Result}; 7 | use crate::expand::Project; 8 | use crate::manifest::Name; 9 | use crate::rustflags; 10 | use serde_derive::Deserialize; 11 | 12 | #[derive(Deserialize)] 13 | pub struct Metadata { 14 | pub target_directory: PathBuf, 15 | pub workspace_root: PathBuf, 16 | } 17 | 18 | fn raw_cargo() -> Command { 19 | Command::new(option_env!("CARGO").unwrap_or("cargo")) 20 | } 21 | 22 | fn cargo(project: &Project) -> Command { 23 | let mut cmd = raw_cargo(); 24 | cmd.current_dir(&project.dir); 25 | cmd.env("CARGO_TARGET_DIR", &project.inner_target_dir); 26 | rustflags::set_env(&mut cmd); 27 | cmd 28 | } 29 | 30 | pub(crate) fn metadata() -> Result { 31 | let output = raw_cargo() 32 | .arg("metadata") 33 | .arg("--format-version=1") 34 | .output() 35 | .map_err(Error::Cargo)?; 36 | 37 | serde_json::from_slice(&output.stdout).map_err(Error::CargoMetadata) 38 | } 39 | 40 | pub(crate) fn expand( 41 | project: &Project, 42 | name: &Name, 43 | args: &Option, 44 | ) -> Result<(bool, Vec)> 45 | where 46 | I: IntoIterator + Clone, 47 | S: AsRef, 48 | { 49 | let mut cargo = cargo(project); 50 | let cargo = cargo 51 | .arg("expand") 52 | .arg("--bin") 53 | .arg(name.as_ref()) 54 | .arg("--theme") 55 | .arg("none"); 56 | 57 | if let Some(args) = args { 58 | cargo.args(args.clone()); 59 | } 60 | 61 | let cargo_expand = cargo 62 | .output() 63 | .map_err(|e| Error::CargoExpandExecution(e.to_string()))?; 64 | 65 | if !cargo_expand.status.success() { 66 | return Ok((false, cargo_expand.stderr)); 67 | } 68 | 69 | Ok((true, cargo_expand.stdout)) 70 | } 71 | 72 | /// Builds dependencies for macro expansion and pipes `cargo` output to `STDOUT`. 73 | /// Tries to expand macros in `main.rs` and intentionally filters the result. 74 | /// This function is called before macro expansions to speed them up and 75 | /// for dependencies build process to be visible for user. 76 | pub(crate) fn build_dependencies(project: &Project) -> Result<()> { 77 | use std::io::Write; 78 | 79 | let stdout = cargo(project) 80 | .arg("expand") 81 | .arg("--bin") 82 | .arg(project.name.clone()) 83 | .arg("--theme") 84 | .arg("none") 85 | .stdout(std::process::Stdio::piped()) 86 | .spawn()? 87 | .stdout 88 | .ok_or(Error::CargoFail)?; 89 | 90 | let reader = std::io::BufReader::new(stdout); 91 | 92 | // Filter ignored lines and main.rs content 93 | reader 94 | .lines() 95 | .filter_map(|line| line.ok()) 96 | .filter(|line| !line.starts_with("fn main() {}")) 97 | .filter(|line| !line_should_be_ignored(line)) 98 | .for_each(|line| { 99 | let _ = writeln!(std::io::stdout(), "{}", line); 100 | }); 101 | 102 | Ok(()) 103 | } 104 | 105 | const IGNORED_LINES: [&str; 5] = [ 106 | "#![feature(prelude_import)]", 107 | "#[prelude_import]", 108 | "use std::prelude::", 109 | "#[macro_use]", 110 | "extern crate std;", 111 | ]; 112 | 113 | fn line_should_be_ignored(line: &str) -> bool { 114 | for check in IGNORED_LINES.iter() { 115 | if line.starts_with(check) { 116 | return true; 117 | } 118 | } 119 | 120 | false 121 | } 122 | -------------------------------------------------------------------------------- /src/dependencies.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::manifest::Edition; 3 | use serde::de::value::MapAccessDeserializer; 4 | use serde::de::{self, Deserialize, Deserializer, Visitor}; 5 | use serde::ser::{Serialize, Serializer}; 6 | use serde_derive::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use std::collections::BTreeMap as Map; 9 | use std::fmt; 10 | use std::fs; 11 | use std::path::Path; 12 | use std::path::PathBuf; 13 | 14 | pub(crate) fn get_manifest(manifest_dir: &Path) -> Manifest { 15 | try_get_manifest(manifest_dir).unwrap_or_default() 16 | } 17 | 18 | fn try_get_manifest(manifest_dir: &Path) -> Result { 19 | let cargo_toml_path = manifest_dir.join("Cargo.toml"); 20 | let manifest_str = fs::read_to_string(cargo_toml_path)?; 21 | let mut manifest: Manifest = toml_edit::de::from_str(&manifest_str)?; 22 | 23 | fix_dependencies(&mut manifest.dependencies, manifest_dir); 24 | fix_dependencies(&mut manifest.dev_dependencies, manifest_dir); 25 | 26 | Ok(manifest) 27 | } 28 | 29 | pub(crate) fn get_workspace_manifest(manifest_dir: &Path) -> WorkspaceManifest { 30 | try_get_workspace_manifest(manifest_dir).unwrap_or_default() 31 | } 32 | 33 | pub(crate) fn try_get_workspace_manifest(manifest_dir: &Path) -> Result { 34 | let cargo_toml_path = manifest_dir.join("Cargo.toml"); 35 | let manifest_str = fs::read_to_string(cargo_toml_path)?; 36 | let mut manifest: WorkspaceManifest = toml_edit::de::from_str(&manifest_str)?; 37 | 38 | fix_dependencies(&mut manifest.workspace.dependencies, manifest_dir); 39 | fix_patches(&mut manifest.patch, manifest_dir); 40 | fix_replacements(&mut manifest.replace, manifest_dir); 41 | 42 | Ok(manifest) 43 | } 44 | 45 | fn fix_dependencies(dependencies: &mut Map, dir: &Path) { 46 | dependencies.remove("macrotest"); 47 | for dep in dependencies.values_mut() { 48 | dep.path = dep.path.as_ref().map(|path| dir.join(path)); 49 | } 50 | } 51 | 52 | fn fix_patches(patches: &mut Map, dir: &Path) { 53 | for registry in patches.values_mut() { 54 | registry.crates.remove("macrotest"); 55 | for patch in registry.crates.values_mut() { 56 | patch.path = patch.path.as_ref().map(|path| dir.join(path)); 57 | } 58 | } 59 | } 60 | 61 | fn fix_replacements(replacements: &mut Map, dir: &Path) { 62 | replacements.remove("macrotest"); 63 | for replacement in replacements.values_mut() { 64 | replacement.path = replacement.path.as_ref().map(|path| dir.join(path)); 65 | } 66 | } 67 | 68 | #[derive(Deserialize, Default, Debug)] 69 | pub struct WorkspaceManifest { 70 | #[serde(default)] 71 | pub workspace: Workspace, 72 | #[serde(default)] 73 | pub patch: Map, 74 | #[serde(default)] 75 | pub replace: Map, 76 | } 77 | 78 | #[derive(Deserialize, Default, Debug)] 79 | pub struct Workspace { 80 | #[serde(default)] 81 | pub package: WorkspacePackage, 82 | #[serde(default)] 83 | pub dependencies: Map, 84 | } 85 | 86 | #[derive(Deserialize, Default, Debug)] 87 | pub struct WorkspacePackage { 88 | pub edition: Option, 89 | } 90 | 91 | #[derive(Deserialize, Default, Debug)] 92 | pub struct Manifest { 93 | #[serde(default, rename = "cargo-features")] 94 | pub cargo_features: Vec, 95 | #[serde(default)] 96 | pub package: Package, 97 | #[serde(default)] 98 | pub features: Map>, 99 | #[serde(default)] 100 | pub dependencies: Map, 101 | #[serde(default, alias = "dev-dependencies")] 102 | pub dev_dependencies: Map, 103 | } 104 | 105 | #[derive(Deserialize, Default, Debug)] 106 | pub struct Package { 107 | #[serde(default)] 108 | pub edition: Edition, 109 | } 110 | 111 | #[derive(Serialize, Deserialize, Clone, Debug)] 112 | #[serde(remote = "Self")] 113 | pub struct Dependency { 114 | #[serde(skip_serializing_if = "Option::is_none")] 115 | pub version: Option, 116 | #[serde(skip_serializing_if = "Option::is_none")] 117 | pub path: Option, 118 | #[serde( 119 | rename = "default-features", 120 | default = "get_true", 121 | skip_serializing_if = "is_true" 122 | )] 123 | pub default_features: bool, 124 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 125 | pub features: Vec, 126 | #[serde(default, skip_serializing_if = "is_false")] 127 | pub workspace: bool, 128 | #[serde(flatten)] 129 | pub rest: Map, 130 | } 131 | 132 | #[derive(Serialize, Deserialize, Clone, Debug)] 133 | #[serde(transparent)] 134 | pub struct RegistryPatch { 135 | crates: Map, 136 | } 137 | 138 | #[derive(Serialize, Deserialize, Clone, Debug)] 139 | pub struct Patch { 140 | #[serde(skip_serializing_if = "Option::is_none")] 141 | pub path: Option, 142 | #[serde(skip_serializing_if = "Option::is_none")] 143 | pub git: Option, 144 | #[serde(skip_serializing_if = "Option::is_none")] 145 | pub branch: Option, 146 | } 147 | 148 | fn get_true() -> bool { 149 | true 150 | } 151 | 152 | #[allow(clippy::trivially_copy_pass_by_ref)] 153 | fn is_true(boolean: &bool) -> bool { 154 | *boolean 155 | } 156 | 157 | #[allow(clippy::trivially_copy_pass_by_ref)] 158 | fn is_false(boolean: &bool) -> bool { 159 | !*boolean 160 | } 161 | 162 | impl Serialize for Dependency { 163 | fn serialize(&self, serializer: S) -> Result 164 | where 165 | S: Serializer, 166 | { 167 | Dependency::serialize(self, serializer) 168 | } 169 | } 170 | 171 | impl<'de> Deserialize<'de> for Dependency { 172 | fn deserialize(deserializer: D) -> Result 173 | where 174 | D: Deserializer<'de>, 175 | { 176 | struct DependencyVisitor; 177 | 178 | impl<'de> Visitor<'de> for DependencyVisitor { 179 | type Value = Dependency; 180 | 181 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 182 | formatter.write_str( 183 | "a version string like \"0.9.8\" or a \ 184 | dependency like { version = \"0.9.8\" }", 185 | ) 186 | } 187 | 188 | fn visit_str(self, s: &str) -> Result 189 | where 190 | E: de::Error, 191 | { 192 | Ok(Dependency { 193 | version: Some(s.to_owned()), 194 | path: None, 195 | default_features: true, 196 | features: Vec::new(), 197 | workspace: false, 198 | rest: Map::new(), 199 | }) 200 | } 201 | 202 | fn visit_map(self, map: M) -> Result 203 | where 204 | M: de::MapAccess<'de>, 205 | { 206 | Dependency::deserialize(MapAccessDeserializer::new(map)) 207 | } 208 | } 209 | 210 | deserializer.deserialize_any(DependencyVisitor) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub(crate) enum Error { 3 | Cargo(std::io::Error), 4 | CargoExpandExecution(String), 5 | CargoFail, 6 | CargoMetadata(serde_json::error::Error), 7 | Io(std::io::Error), 8 | TomlSer(toml_edit::ser::Error), 9 | TomlDe(toml_edit::de::Error), 10 | Glob(glob::GlobError), 11 | GlobPattern(glob::PatternError), 12 | ManifestDir, 13 | PkgName, 14 | UnrecognizedEnv(std::ffi::OsString), 15 | } 16 | 17 | pub(crate) type Result = std::result::Result; 18 | 19 | impl std::fmt::Display for Error { 20 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 21 | use self::Error::*; 22 | 23 | match self { 24 | Cargo(e) => write!(f, "{}", e), 25 | CargoExpandExecution(e) => write!(f, "Failed to execute cargo command: {}", e), 26 | CargoFail => write!(f, "cargo reported an error"), 27 | CargoMetadata(e) => write!(f, "{}", e), 28 | Io(e) => write!(f, "{}", e), 29 | TomlSer(e) => write!(f, "{}", e), 30 | TomlDe(e) => write!(f, "{}", e), 31 | Glob(e) => write!(f, "{}", e), 32 | GlobPattern(e) => write!(f, "{}", e), 33 | ManifestDir => write!(f, "could not find CARGO_MANIFEST_DIR env var"), 34 | PkgName => write!(f, "could not find CARGO_PKG_NAME env var"), 35 | UnrecognizedEnv(e) => write!( 36 | f, 37 | "unrecognized value of MACROTEST: \"{}\"", 38 | e.to_string_lossy() 39 | ), 40 | } 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(e: std::io::Error) -> Self { 46 | Error::Io(e) 47 | } 48 | } 49 | 50 | impl From for Error { 51 | fn from(e: toml_edit::ser::Error) -> Self { 52 | Error::TomlSer(e) 53 | } 54 | } 55 | 56 | impl From for Error { 57 | fn from(e: toml_edit::de::Error) -> Self { 58 | Error::TomlDe(e) 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(e: glob::GlobError) -> Self { 64 | Error::Glob(e) 65 | } 66 | } 67 | 68 | impl From for Error { 69 | fn from(e: glob::PatternError) -> Self { 70 | Error::GlobPattern(e) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/expand.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsStr; 3 | use std::fs; 4 | use std::io::Write; 5 | use std::iter; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use crate::cargo; 9 | use crate::dependencies::{self, Dependency}; 10 | use crate::features; 11 | use crate::manifest::{Bin, Build, Config, Manifest, Name, Package, Workspace}; 12 | use crate::message::{message_different, message_expansion_error}; 13 | use crate::rustflags; 14 | use crate::{error::Error, error::Result}; 15 | use syn::punctuated::Punctuated; 16 | use syn::{Item, Meta, Token}; 17 | 18 | /// An extension for files containing `cargo expand` result. 19 | const EXPANDED_RS_SUFFIX: &str = "expanded.rs"; 20 | 21 | #[derive(Debug)] 22 | pub(crate) struct Project { 23 | pub dir: PathBuf, 24 | source_dir: PathBuf, 25 | /// Used for the inner runs of cargo() 26 | pub inner_target_dir: PathBuf, 27 | pub name: String, 28 | pub features: Option>, 29 | workspace: PathBuf, 30 | overwrite: bool, 31 | } 32 | 33 | /// This `Drop` implementation will clean up the temporary crates when expansion is finished. 34 | /// This is to prevent pollution of the filesystem with dormant files. 35 | impl 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 | 47 | /// Attempts to expand macros in files that match glob pattern. 48 | /// 49 | /// # Refresh behavior 50 | /// 51 | /// If no matching `.expanded.rs` files present, they will be created and result of expansion 52 | /// will be written into them. 53 | /// 54 | /// # Panics 55 | /// 56 | /// Will panic if matching `.expanded.rs` file is present, but has different expanded code in it. 57 | pub fn expand(path: impl AsRef) { 58 | run_tests( 59 | path, 60 | ExpansionBehavior::RegenerateFiles, 61 | Option::>::None, 62 | ); 63 | } 64 | 65 | /// Same as [`expand`] but allows to pass additional arguments to `cargo-expand`. 66 | /// 67 | /// [`expand`]: expand/fn.expand.html 68 | pub fn expand_args(path: impl AsRef, args: I) 69 | where 70 | I: IntoIterator + Clone, 71 | S: AsRef, 72 | { 73 | run_tests(path, ExpansionBehavior::RegenerateFiles, Some(args)); 74 | } 75 | 76 | /// Attempts to expand macros in files that match glob pattern. 77 | /// More strict version of [`expand`] function. 78 | /// 79 | /// # Refresh behavior 80 | /// 81 | /// If no matching `.expanded.rs` files present, it considered a failed test. 82 | /// 83 | /// # Panics 84 | /// 85 | /// Will panic if no matching `.expanded.rs` file is present. Otherwise it will exhibit the same 86 | /// behavior as in [`expand`]. 87 | /// 88 | /// [`expand`]: expand/fn.expand.html 89 | pub fn expand_without_refresh(path: impl AsRef) { 90 | run_tests( 91 | path, 92 | ExpansionBehavior::ExpectFiles, 93 | Option::>::None, 94 | ); 95 | } 96 | 97 | /// Same as [`expand_without_refresh`] but allows to pass additional arguments to `cargo-expand`. 98 | /// 99 | /// [`expand_without_refresh`]: expand/fn.expand_without_refresh.html 100 | pub fn expand_without_refresh_args(path: impl AsRef, args: I) 101 | where 102 | I: IntoIterator + Clone, 103 | S: AsRef, 104 | { 105 | run_tests(path, ExpansionBehavior::ExpectFiles, Some(args)); 106 | } 107 | 108 | #[derive(Debug, Copy, Clone)] 109 | enum ExpansionBehavior { 110 | RegenerateFiles, 111 | ExpectFiles, 112 | } 113 | 114 | fn run_tests(path: impl AsRef, expansion_behavior: ExpansionBehavior, args: Option) 115 | where 116 | I: IntoIterator + Clone, 117 | S: AsRef, 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::>(); 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 | 178 | fn prepare(tests: &[ExpandedTest]) -> Result { 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 | // Use random string for the crate dir to 198 | // prevent conflicts when running parallel tests. 199 | 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 | // Remove remaining artifacts from previous runs if exist. 203 | // For example, if the user stops the test with Ctrl-C during a previous 204 | // run, the destructor of Project will not be called. 205 | 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_edit::ser::to_string(&manifest)?; 222 | 223 | let config = make_config(); 224 | let config_toml = toml_edit::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 | 248 | fn make_manifest( 249 | crate_name: String, 250 | project: &Project, 251 | tests: &[ExpandedTest], 252 | ) -> Result { 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 | // Within a workspace, only the [patch] and [replace] sections in 290 | // the workspace root's Cargo.toml are applied by Cargo. 291 | 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 | 328 | fn make_config() -> Config { 329 | Config { 330 | build: Build { 331 | rustflags: rustflags::make_vec(), 332 | }, 333 | } 334 | } 335 | 336 | #[derive(Debug)] 337 | enum ExpansionOutcome { 338 | Same, 339 | Different(Vec, Vec), 340 | Update, 341 | ExpandError(Vec), 342 | NoExpandedFileFound, 343 | } 344 | 345 | struct ExpandedTest { 346 | name: Name, 347 | test: PathBuf, 348 | error: Option, 349 | } 350 | 351 | impl ExpandedTest { 352 | pub fn run( 353 | &self, 354 | project: &Project, 355 | expansion_behavior: ExpansionBehavior, 356 | args: &Option, 357 | ) -> Result 358 | where 359 | I: IntoIterator + Clone, 360 | S: AsRef, 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 | // Write a .expanded.rs file contents 386 | 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 | // Write a .expanded.rs file contents 402 | 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(); // Use normalized text for a message 411 | ExpansionOutcome::Different(expected_expansion_bytes, output_bytes) 412 | }) 413 | } 414 | } 415 | 416 | fn 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 | // Strip the following: 424 | // 425 | // #![feature(prelude_import)] 426 | // 427 | 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::::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 | // Strip the following: 447 | // 448 | // #[prelude_import] 449 | // use std::prelude::$edition::*; 450 | // 451 | // #[macro_use] 452 | // extern crate std; 453 | // 454 | 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 | 473 | fn expand_globs(path: impl AsRef) -> Vec { 474 | fn glob(pattern: &str) -> Result> { 475 | let mut paths = glob::glob(pattern)? 476 | .map(|entry| entry.map_err(Error::from)) 477 | .collect::>>()?; 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 | } 521 | -------------------------------------------------------------------------------- /src/features.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{self, Deserialize, DeserializeOwned, Deserializer}; 2 | use serde_derive::Deserialize; 3 | use std::env; 4 | use std::error::Error; 5 | use std::ffi::OsStr; 6 | use std::fs; 7 | use std::path::PathBuf; 8 | 9 | pub fn find() -> Option> { 10 | try_find().ok() 11 | } 12 | 13 | struct Ignored; 14 | 15 | impl From for Ignored { 16 | fn from(_error: E) -> Self { 17 | Ignored 18 | } 19 | } 20 | 21 | #[derive(Deserialize)] 22 | struct Build { 23 | #[serde(deserialize_with = "from_json")] 24 | features: Vec, 25 | } 26 | 27 | fn try_find() -> Result, Ignored> { 28 | // This will look something like: 29 | // /path/to/crate_name/target/debug/deps/test_name-HASH 30 | let test_binary = env::args_os().next().ok_or(Ignored)?; 31 | 32 | // The hash at the end is ascii so not lossy, rest of conversion doesn't 33 | // matter. 34 | let test_binary_lossy = test_binary.to_string_lossy(); 35 | let hash = test_binary_lossy 36 | .get(test_binary_lossy.len() - 17..) 37 | .ok_or(Ignored)?; 38 | if !hash.starts_with('-') || !hash[1..].bytes().all(is_lower_hex_digit) { 39 | return Err(Ignored); 40 | } 41 | 42 | let binary_path = PathBuf::from(&test_binary); 43 | 44 | // Feature selection is saved in: 45 | // /path/to/crate_name/target/debug/.fingerprint/*-HASH/*-HASH.json 46 | let up = binary_path 47 | .parent() 48 | .ok_or(Ignored)? 49 | .parent() 50 | .ok_or(Ignored)?; 51 | let fingerprint_dir = up.join(".fingerprint"); 52 | if !fingerprint_dir.is_dir() { 53 | return Err(Ignored); 54 | } 55 | 56 | let mut hash_matches = Vec::new(); 57 | for entry in fingerprint_dir.read_dir()? { 58 | let entry = entry?; 59 | let is_dir = entry.file_type()?.is_dir(); 60 | let matching_hash = entry.file_name().to_string_lossy().ends_with(hash); 61 | if is_dir && matching_hash { 62 | hash_matches.push(entry.path()); 63 | } 64 | } 65 | 66 | if hash_matches.len() != 1 { 67 | return Err(Ignored); 68 | } 69 | 70 | let mut json_matches = Vec::new(); 71 | for entry in hash_matches[0].read_dir()? { 72 | let entry = entry?; 73 | let is_file = entry.file_type()?.is_file(); 74 | let is_json = entry.path().extension() == Some(OsStr::new("json")); 75 | if is_file && is_json { 76 | json_matches.push(entry.path()); 77 | } 78 | } 79 | 80 | if json_matches.len() != 1 { 81 | return Err(Ignored); 82 | } 83 | 84 | let build_json = fs::read_to_string(&json_matches[0])?; 85 | let build: Build = serde_json::from_str(&build_json)?; 86 | Ok(build.features) 87 | } 88 | 89 | fn is_lower_hex_digit(byte: u8) -> bool { 90 | matches!(byte, b'0'..=b'9' | b'a'..=b'f') 91 | } 92 | 93 | fn from_json<'de, T, D>(deserializer: D) -> Result 94 | where 95 | T: DeserializeOwned, 96 | D: Deserializer<'de>, 97 | { 98 | let json = String::deserialize(deserializer)?; 99 | serde_json::from_str(&json).map_err(de::Error::custom) 100 | } 101 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_type = "lib"] 2 | 3 | //! ####   Test harness for macro expansion. 4 | //! 5 | //! Similar to [trybuild], but allows you to write tests on how macros are expanded. 6 | //! 7 | //! *Minimal Supported Rust Version: 1.65* 8 | //! 9 | //!
10 | //! 11 | //! # Macro expansion tests 12 | //! 13 | //! A minimal `macrotest` setup looks like this: 14 | //! 15 | //! ```rust 16 | //! # /* 17 | //! #[test] 18 | //! # */ 19 | //! pub fn pass() { 20 | //! macrotest::expand("tests/expand/*.rs"); 21 | //! // Alternatively, 22 | //! macrotest::expand_without_refresh("tests/expand/*.rs"); 23 | //! } 24 | //! ``` 25 | //! 26 | //! The test can be run with `cargo test`. This test will invoke the [`cargo expand`] command 27 | //! on each of the source files that matches the glob pattern and will compare the expansion result 28 | //! with the corresponding `*.expanded.rs` file. 29 | //! 30 | //! If a `*.expanded.rs` file doesn't exists and it's not explicitly expected to (see [`expand_without_refresh`]), 31 | //! it will be created (this is how you update your tests). 32 | //! 33 | //! Possible test outcomes are: 34 | //! - **Pass**: expansion succeeded and the result is the same as in the `.expanded.rs` file 35 | //! - **Fail**: expansion was different from the `.expanded.rs` file content 36 | //! - **Refresh**: `.expanded.rs` didn't exist and has been created 37 | //! - **Refresh-fail**: `.expanded.rs` is expected to be present, but not exists. See [`expand_without_refresh`]. 38 | //! 39 | //! *Note:* when working with multiple expansion test files, it is recommended to 40 | //! specify wildcard (*.rs) instead of doing a multiple calls to `expand` functions for individual files. 41 | //! Usage of wildcards for multiple files will group them under a single temporary crate for which 42 | //! dependencies will be built a single time. In contrast, calling `expand` functions for each 43 | //! source file will create multiple temporary crates and that will reduce performance as depdendencies 44 | //! will be build for each of the temporary crates. 45 | //! 46 | //! ## Passing additional arguments to `cargo expand` 47 | //! 48 | //! It's possible to specify additional arguments for [`cargo expand`] command. 49 | //! 50 | //! In order to do so, use the following functions with `_args` suffix: 51 | //! - [`expand_args`] 52 | //! - [`expand_without_refresh_args`] 53 | //! 54 | //! Example: 55 | //! 56 | //! ```rust 57 | //! pub fn pass() { 58 | //! macrotest::expand_args("tests/expand/*.rs", &["--features", "my-feature"]); 59 | //! // Or 60 | //! macrotest::expand_without_refresh_args("tests/expand/*.rs", &["--features", "my-feature"]); 61 | //! } 62 | //! ``` 63 | //! 64 | //! The `_args` functions will result in the following [`cargo expand`] command being run: 65 | //! 66 | //! ```bash 67 | //! cargo expand --bin --theme none --features my-feature 68 | //! ``` 69 | //! 70 | //! # Workflow 71 | //! 72 | //! First of all, the [`cargo expand`] tool must be present. You can install it via cargo: 73 | //! 74 | //! ```bash 75 | //! cargo install --locked cargo-expand 76 | //! ``` 77 | //! 78 | //! (In CI, you'll want to pin to a particular version, 79 | //! since 80 | //! [cargo expand's output is not stable across versions](https://github.com/dtolnay/cargo-expand/issues/179). 81 | //! Look up the 82 | //! [current version](https://crates.io/crates/cargo-expand) 83 | //! and do something like `cargo install --locked --version 1.0.81 cargo-expand`.) 84 | //! 85 | //! ## Setting up a test project 86 | //! 87 | //! In your crate that provides procedural or declarative macros, under the `tests` directory, 88 | //! create an `expand` directory and populate it with different expansion test cases as 89 | //! rust source files. 90 | //! 91 | //! Then create a `tests.rs` file that will run the tests: 92 | //! 93 | //! ```rust 94 | //! # /* 95 | //! #[test] 96 | //! # */ 97 | //! pub fn pass() { 98 | //! macrotest::expand("tests/expand/*.rs"); 99 | //! // Or: 100 | //! macrotest::expand_without_refresh("tests/expand/*.rs"); 101 | //! } 102 | //! ``` 103 | //! 104 | //! And then you can run `cargo test`, which will 105 | //! 106 | //! 1. Expand macros in source files that match glob pattern 107 | //! 1. In case if [`expand`] function is used: 108 | //! - On the first run, generate the `*.expanded.rs` files for each of the test cases under 109 | //! the `expand` directory 110 | //! - On subsequent runs, compare test cases' expansion result with the 111 | //! content of the respective `*.expanded.rs` files 112 | //! 1. In case if [`expand_without_refresh`] is used: 113 | //! - On each run, it will compare test cases' expansion result with the content of the 114 | //! respective `*.expanded.rs` files. 115 | //! - If one or more `*.expanded.rs` files is not found, the test will fail. 116 | //! 117 | //! ## Updating `.expanded.rs` 118 | //! 119 | //! This applicable only to tests that are using [`expand`] or [`expand_args`] function. 120 | //! 121 | //! Run tests with the environment variable `MACROTEST=overwrite` or remove the `*.expanded.rs` 122 | //! files and re-run the corresponding tests. Files will be created automatically; hand-writing 123 | //! them is not recommended. 124 | //! 125 | //! [`expand_without_refresh`]: expand/fn.expand_without_refresh.html 126 | //! [`expand_without_refresh_args`]: expand/fn.expand_without_refresh_args.html 127 | //! [`expand`]: expand/fn.expand.html 128 | //! [`expand_args`]: expand/fn.expand_args.html 129 | //! [trybuild]: https://github.com/dtolnay/trybuild 130 | //! [`cargo expand`]: https://github.com/dtolnay/cargo-expand 131 | 132 | #![allow(clippy::lines_filter_map_ok)] // https://github.com/rust-lang/rust-clippy/issues/14127 133 | 134 | #[macro_use] 135 | mod path; 136 | 137 | mod cargo; 138 | mod dependencies; 139 | mod error; 140 | mod expand; 141 | mod features; 142 | mod manifest; 143 | mod message; 144 | mod rustflags; 145 | 146 | pub use expand::expand; 147 | pub use expand::expand_args; 148 | pub use expand::expand_without_refresh; 149 | pub use expand::expand_without_refresh_args; 150 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | use crate::dependencies::{Dependency, Patch, RegistryPatch}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | use serde_json::Value; 4 | use std::collections::BTreeMap as Map; 5 | use std::ffi::OsStr; 6 | use std::path::PathBuf; 7 | 8 | #[derive(Serialize, Debug)] 9 | pub struct Manifest { 10 | #[serde(rename = "cargo-features")] 11 | #[serde(skip_serializing_if = "Vec::is_empty")] 12 | pub cargo_features: Vec, 13 | pub package: Package, 14 | #[serde(skip_serializing_if = "Map::is_empty")] 15 | pub features: Map>, 16 | pub dependencies: Map, 17 | #[serde(rename = "bin")] 18 | pub bins: Vec, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub workspace: Option, 21 | #[serde(skip_serializing_if = "Map::is_empty")] 22 | pub patch: Map, 23 | #[serde(skip_serializing_if = "Map::is_empty")] 24 | pub replace: Map, 25 | } 26 | 27 | #[derive(Serialize, Debug)] 28 | pub struct Package { 29 | pub name: String, 30 | pub version: String, 31 | pub publish: bool, 32 | pub edition: Edition, 33 | } 34 | 35 | // Do not use enum for edition for future-compatibility. 36 | #[derive(Serialize, Deserialize, Debug)] 37 | pub struct Edition(pub Value); 38 | 39 | #[derive(Serialize, Debug)] 40 | pub struct Bin { 41 | pub name: Name, 42 | pub path: PathBuf, 43 | } 44 | 45 | #[derive(Serialize, Clone, Debug)] 46 | pub struct Name(pub String); 47 | 48 | #[derive(Serialize, Debug)] 49 | pub struct Config { 50 | pub build: Build, 51 | } 52 | 53 | #[derive(Serialize, Debug)] 54 | pub struct Build { 55 | pub rustflags: Vec, 56 | } 57 | 58 | #[derive(Serialize, Debug)] 59 | pub struct Workspace { 60 | #[serde(skip_serializing_if = "WorkspacePackage::is_none")] 61 | pub package: WorkspacePackage, 62 | #[serde(skip_serializing_if = "Map::is_empty")] 63 | pub dependencies: Map, 64 | } 65 | 66 | #[derive(Serialize, Debug)] 67 | pub struct WorkspacePackage { 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub edition: Option, 70 | } 71 | 72 | impl WorkspacePackage { 73 | fn is_none(&self) -> bool { 74 | self.edition.is_none() 75 | } 76 | } 77 | 78 | impl Default for Edition { 79 | fn default() -> Self { 80 | Self("2021".into()) 81 | } 82 | } 83 | 84 | impl AsRef for Name { 85 | fn as_ref(&self) -> &OsStr { 86 | self.0.as_ref() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use diff::Result; 2 | 3 | /// Prints the difference of the two snippets of expanded code. 4 | pub(crate) fn message_different(name: &str, a: &[u8], b: &[u8]) { 5 | let a = String::from_utf8_lossy(a); 6 | let b = String::from_utf8_lossy(b); 7 | 8 | let changes = diff::lines(&a, &b); 9 | 10 | let mut lines_added = 0; 11 | let mut lines_removed = 0; 12 | for diff in &changes { 13 | match diff { 14 | Result::Left(_) => lines_added += 1, 15 | Result::Right(_) => lines_removed += 1, 16 | _ => (), 17 | } 18 | } 19 | 20 | eprintln!("{} - different!", name); 21 | 22 | eprintln!( 23 | "Diff [lines: {} added, {} removed]:", 24 | lines_added, lines_removed 25 | ); 26 | eprintln!("--------------------------"); 27 | 28 | for change in changes { 29 | match change { 30 | Result::Both(x, _) => { 31 | eprintln!(" {}", x); 32 | } 33 | Result::Left(x) => { 34 | eprintln!("+{}", x); 35 | } 36 | Result::Right(x) => { 37 | eprintln!("-{}", x); 38 | } 39 | } 40 | } 41 | 42 | eprintln!("--------------------------"); 43 | } 44 | 45 | /// Prints an error from `cargo expand` invocation. 46 | /// Makes some suggestions when possible. 47 | pub(crate) fn message_expansion_error(msg: Vec) { 48 | let msg = String::from_utf8(msg); 49 | 50 | eprintln!("Expansion error:"); 51 | if let Ok(msg) = msg { 52 | eprintln!("{}", msg); 53 | 54 | // No `cargo expand` subcommand installed, make a suggestion 55 | if msg.contains("no such subcommand: `expand`") { 56 | eprintln!("Perhaps, `cargo expand` is not installed?"); 57 | eprintln!("Install it by running:"); 58 | eprintln!(); 59 | eprintln!("\tcargo install cargo-expand"); 60 | eprintln!(); 61 | } 62 | } else { 63 | eprintln!(""); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | // credits: dtolnay 2 | 3 | macro_rules! path { 4 | ($($tt:tt)+) => { 5 | tokenize_path!([] [] $($tt)+) 6 | }; 7 | } 8 | 9 | // Private implementation detail. 10 | macro_rules! tokenize_path { 11 | ([$(($($component:tt)+))*] [$($cur:tt)+] / $($rest:tt)+) => { 12 | tokenize_path!([$(($($component)+))* ($($cur)+)] [] $($rest)+) 13 | }; 14 | 15 | ([$(($($component:tt)+))*] [$($cur:tt)*] $first:tt $($rest:tt)*) => { 16 | tokenize_path!([$(($($component)+))*] [$($cur)* $first] $($rest)*) 17 | }; 18 | 19 | ([$(($($component:tt)+))*] [$($cur:tt)+]) => { 20 | tokenize_path!([$(($($component)+))* ($($cur)+)]) 21 | }; 22 | 23 | ([$(($($component:tt)+))*]) => {{ 24 | let mut path = std::path::PathBuf::new(); 25 | $( 26 | path.push(&($($component)+)); 27 | )* 28 | path 29 | }}; 30 | } 31 | 32 | #[test] 33 | fn test_path_macro() { 34 | use std::path::{Path, PathBuf}; 35 | 36 | struct Project { 37 | dir: PathBuf, 38 | } 39 | 40 | let project = Project { 41 | dir: PathBuf::from("../target/tests"), 42 | }; 43 | 44 | let cargo_dir = path!(project.dir / ".cargo" / "config.toml"); 45 | assert_eq!(cargo_dir, Path::new("../target/tests/.cargo/config.toml")); 46 | } 47 | -------------------------------------------------------------------------------- /src/rustflags.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::Command; 3 | 4 | const CARGO_ENCODED_RUSTFLAGS: &str = "CARGO_ENCODED_RUSTFLAGS"; 5 | const RUSTFLAGS: &str = "RUSTFLAGS"; 6 | const IGNORED_LINTS: &[&str] = &["dead_code"]; 7 | 8 | pub fn make_vec() -> Vec { 9 | let mut rustflags = Vec::new(); 10 | 11 | for &lint in IGNORED_LINTS { 12 | rustflags.push("-A".to_owned()); 13 | rustflags.push(lint.to_owned()); 14 | } 15 | 16 | rustflags 17 | } 18 | 19 | pub fn set_env(cmd: &mut Command) { 20 | // The precedence of rustflags is: 21 | // 1. CARGO_ENCODED_RUSTFLAGS 22 | // 2. RUSTFLAGS 23 | // 3. target..rustflags (CARGO_TARGET__RUSTFLAGS) and target..rustflags 24 | // 4. build.rustflags (CARGO_BUILD_RUSTFLAGS) 25 | // Refs: https://doc.rust-lang.org/nightly/cargo/reference/config.html#buildrustflags 26 | // For now, skip 3 and 4 because 3 is complex and handling 4 without 3 incorrectly overwrite rustflags. 27 | // TODO: Consider using cargo-config2 crate that implements it. 28 | let (key, mut val, separator) = match env::var_os(CARGO_ENCODED_RUSTFLAGS) { 29 | Some(val) => (CARGO_ENCODED_RUSTFLAGS, val, "\x1f"), 30 | None => match env::var_os(RUSTFLAGS) { 31 | Some(val) => (RUSTFLAGS, val, " "), 32 | None => return, 33 | }, 34 | }; 35 | 36 | for flag in make_vec() { 37 | if !val.is_empty() { 38 | val.push(separator); 39 | } 40 | val.push(flag); 41 | } 42 | 43 | cmd.env(key, val); 44 | } 45 | -------------------------------------------------------------------------------- /test-procmacro-project/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /test-procmacro-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test_procmacro_project" 3 | version = "0.1.0" 4 | authors = ["eupn "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | quote = "1" 12 | proc-macro2 = "1.0" 13 | syn = "2" 14 | 15 | [dev-dependencies] 16 | macrotest = { path = "../" } 17 | -------------------------------------------------------------------------------- /test-procmacro-project/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use proc_macro::TokenStream; 3 | 4 | use quote::quote; 5 | use syn::{parse_macro_input, DeriveInput}; 6 | 7 | /// Example of [function-like procedural macro][1]. 8 | /// 9 | /// [1]: https://doc.rust-lang.org/reference/procedural-macros.html#function-like-procedural-macros 10 | #[proc_macro] 11 | pub fn my_macro(input: TokenStream) -> TokenStream { 12 | let input = parse_macro_input!(input as DeriveInput); 13 | 14 | let tokens = quote! { 15 | #input 16 | 17 | struct Hello; 18 | }; 19 | 20 | tokens.into() 21 | } 22 | 23 | /// Example of user-defined [derive mode macro][1] 24 | /// 25 | /// [1]: https://doc.rust-lang.org/reference/procedural-macros.html#derive-mode-macros 26 | #[proc_macro_derive(MyDerive)] 27 | pub fn my_derive(_input: TokenStream) -> TokenStream { 28 | // Emit test garbage 29 | let tokens = quote! { 30 | struct Hello; 31 | }; 32 | 33 | tokens.into() 34 | } 35 | 36 | /// Example of user-defined [procedural macro attribute][1]. 37 | /// 38 | /// [1]: https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros 39 | #[proc_macro_attribute] 40 | pub fn my_attribute(_args: TokenStream, input: TokenStream) -> TokenStream { 41 | let input = parse_macro_input!(input as DeriveInput); 42 | 43 | let tokens = quote! { 44 | #input 45 | 46 | struct Hello; 47 | }; 48 | 49 | tokens.into() 50 | } 51 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/expand/attribute.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_procmacro_project; 3 | struct Test; 4 | struct Hello; 5 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/expand/attribute.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_procmacro_project; 3 | 4 | #[my_attribute] 5 | struct Test; 6 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/expand/derive.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_procmacro_project; 3 | struct Test; 4 | struct Hello; 5 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/expand/derive.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_procmacro_project; 3 | 4 | #[derive(MyDerive)] 5 | struct Test; 6 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/expand/macro.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_procmacro_project; 3 | pub fn main() { 4 | struct Test; 5 | struct Hello; 6 | } 7 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/expand/macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_procmacro_project; 3 | 4 | pub fn main() { 5 | my_macro! { struct Test; } 6 | } 7 | -------------------------------------------------------------------------------- /test-procmacro-project/tests/tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | pub fn pass() { 3 | macrotest::expand("tests/expand/*.rs"); 4 | } 5 | -------------------------------------------------------------------------------- /test-project/.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /test-project/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "basic-toml" 7 | version = "0.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7bfc506e7a2370ec239e1d072507b2a80c833083699d3c6fa176fbb4de8448c6" 10 | dependencies = [ 11 | "serde", 12 | ] 13 | 14 | [[package]] 15 | name = "diff" 16 | version = "0.1.13" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 19 | 20 | [[package]] 21 | name = "glob" 22 | version = "0.3.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 25 | 26 | [[package]] 27 | name = "itoa" 28 | version = "1.0.9" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 31 | 32 | [[package]] 33 | name = "macrotest" 34 | version = "1.0.9" 35 | dependencies = [ 36 | "basic-toml", 37 | "diff", 38 | "glob", 39 | "prettyplease", 40 | "serde", 41 | "serde_json", 42 | "syn", 43 | ] 44 | 45 | [[package]] 46 | name = "prettyplease" 47 | version = "0.2.12" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" 50 | dependencies = [ 51 | "proc-macro2", 52 | "syn", 53 | ] 54 | 55 | [[package]] 56 | name = "proc-macro2" 57 | version = "1.0.66" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 60 | dependencies = [ 61 | "unicode-ident", 62 | ] 63 | 64 | [[package]] 65 | name = "quote" 66 | version = "1.0.32" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 69 | dependencies = [ 70 | "proc-macro2", 71 | ] 72 | 73 | [[package]] 74 | name = "ryu" 75 | version = "1.0.15" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 78 | 79 | [[package]] 80 | name = "serde" 81 | version = "1.0.183" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" 84 | dependencies = [ 85 | "serde_derive", 86 | ] 87 | 88 | [[package]] 89 | name = "serde_derive" 90 | version = "1.0.183" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" 93 | dependencies = [ 94 | "proc-macro2", 95 | "quote", 96 | "syn", 97 | ] 98 | 99 | [[package]] 100 | name = "serde_json" 101 | version = "1.0.104" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" 104 | dependencies = [ 105 | "itoa", 106 | "ryu", 107 | "serde", 108 | ] 109 | 110 | [[package]] 111 | name = "syn" 112 | version = "2.0.28" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 115 | dependencies = [ 116 | "proc-macro2", 117 | "quote", 118 | "unicode-ident", 119 | ] 120 | 121 | [[package]] 122 | name = "test-project" 123 | version = "0.1.0" 124 | dependencies = [ 125 | "macrotest", 126 | ] 127 | 128 | [[package]] 129 | name = "unicode-ident" 130 | version = "1.0.11" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 133 | -------------------------------------------------------------------------------- /test-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-project" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | 8 | [features] 9 | default = [] 10 | 11 | test-feature = [] 12 | 13 | [dev-dependencies] 14 | macrotest = { path = "../" } 15 | -------------------------------------------------------------------------------- /test-project/README.md: -------------------------------------------------------------------------------- 1 | # `test-project` 2 | 3 | This is an example project that provides a declarative `test_vec![]` macro 4 | to test its expansion under the [tests](tests) directory with [`macrostest`](https://crates.io/crates/macrotest). -------------------------------------------------------------------------------- /test-project/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! test_vec { 3 | () => { 4 | Vec::new() 5 | }; 6 | ( $( $x:expr ),+ ) => { 7 | { 8 | let mut temp_vec = Vec::new(); 9 | $( 10 | temp_vec.push($x); 11 | )* 12 | temp_vec 13 | } 14 | }; 15 | } 16 | 17 | #[cfg(feature = "test-feature")] 18 | #[cfg_attr(feature = "test-feature", macro_export)] 19 | macro_rules! test_feature_vec { 20 | () => { 21 | Vec::new() 22 | }; 23 | ( $( $x:expr ),+ ) => { 24 | { 25 | let mut temp_vec = Vec::new(); 26 | $( 27 | temp_vec.push($x); 28 | )* 29 | temp_vec 30 | } 31 | }; 32 | } -------------------------------------------------------------------------------- /test-project/tests/expand/first.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | pub fn main() { 4 | Vec::new(); 5 | } 6 | -------------------------------------------------------------------------------- /test-project/tests/expand/first.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/expand/fourth.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | pub fn main() { 4 | { 5 | let mut temp_vec = Vec::new(); 6 | temp_vec.push(1); 7 | temp_vec.push(2); 8 | temp_vec.push(3); 9 | temp_vec 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /test-project/tests/expand/fourth.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![1, 2, 3]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/expand/second.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | pub fn main() { 4 | { 5 | let mut temp_vec = Vec::new(); 6 | temp_vec.push(1); 7 | temp_vec 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /test-project/tests/expand/second.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![1]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/expand/third.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | pub fn main() { 4 | { 5 | let mut temp_vec = Vec::new(); 6 | temp_vec.push(1); 7 | temp_vec.push(2); 8 | temp_vec 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /test-project/tests/expand/third.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![1, 2]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/expand_args/with_args.expanded.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-feature")] 2 | #[macro_use] 3 | extern crate test_project; 4 | pub fn main() { 5 | { 6 | let mut temp_vec = Vec::new(); 7 | temp_vec.push(1); 8 | temp_vec.push(2); 9 | temp_vec.push(3); 10 | temp_vec 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /test-project/tests/expand_args/with_args.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-feature")] 2 | 3 | #[macro_use] 4 | extern crate test_project; 5 | 6 | pub fn main() { 7 | test_feature_vec![1, 2, 3]; 8 | } 9 | -------------------------------------------------------------------------------- /test-project/tests/no_expanded/first.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/no_expanded/fourth.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![1, 2, 3]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/no_expanded/second.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![1]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/no_expanded/third.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![1, 2]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/no_expanded_args/with_args.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-feature")] 2 | 3 | #[macro_use] 4 | extern crate test_project; 5 | 6 | pub fn main() { 7 | test_feature_vec![1, 2, 3]; 8 | } 9 | -------------------------------------------------------------------------------- /test-project/tests/par_tests_regression.rs: -------------------------------------------------------------------------------- 1 | // The tests were interfering with each other when run in parallel. 2 | // This regression test module will ensure that parallel use case is handled. 3 | 4 | #[test] 5 | pub fn parallel_1() { 6 | macrotest::expand("tests/expand/first.rs"); 7 | } 8 | 9 | #[test] 10 | pub fn parallel_2() { 11 | macrotest::expand("tests/expand/second.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /test-project/tests/pr61/a/test.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | pub fn main() { 4 | Vec::new(); 5 | } 6 | -------------------------------------------------------------------------------- /test-project/tests/pr61/a/test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/pr61/b/test.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | pub fn main() { 4 | Vec::new(); 5 | } 6 | -------------------------------------------------------------------------------- /test-project/tests/pr61/b/test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate test_project; 3 | 4 | pub fn main() { 5 | test_vec![]; 6 | } 7 | -------------------------------------------------------------------------------- /test-project/tests/tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | pub fn pass() { 3 | macrotest::expand("tests/expand/*.rs"); 4 | } 5 | 6 | #[test] 7 | pub fn pass_expect_expanded() { 8 | // If you delete one of the `.expanded.rs` files, this test will fail. 9 | macrotest::expand_without_refresh("tests/expand/*.rs"); 10 | } 11 | 12 | #[test] 13 | #[should_panic] 14 | pub fn fail_expect_expanded() { 15 | // This directory doesn't have expanded files but since they're expected, the test will fail. 16 | macrotest::expand_without_refresh("tests/no_expanded/*.rs"); 17 | } 18 | 19 | #[test] 20 | pub fn pass_args() { 21 | macrotest::expand_args("tests/expand_args/*.rs", &["--features", "test-feature"]); 22 | } 23 | 24 | #[test] 25 | pub fn pass_expect_expanded_args() { 26 | // If you delete one of the `.expanded.rs` files, this test will fail. 27 | macrotest::expand_args("tests/expand_args/*.rs", &["--features", "test-feature"]); 28 | } 29 | 30 | #[test] 31 | #[should_panic] 32 | pub fn fail_expect_expanded_args() { 33 | // This directory doesn't have expanded files but since they're expected, the test will fail. 34 | macrotest::expand_without_refresh_args( 35 | "tests/no_expanded_args/*.rs", 36 | &["--features", "test-feature"], 37 | ); 38 | } 39 | 40 | // https://github.com/eupn/macrotest/pull/61 41 | #[test] 42 | pub fn pr61() { 43 | macrotest::expand("tests/pr61/*/*.rs"); 44 | } 45 | --------------------------------------------------------------------------------