|
| 1 | +//! Test that macro_rules! definitions with fn items get instrumented correctly. |
| 2 | +//! Covers the full pipeline (build + run + verify NDJSON output) and a |
| 3 | +//! unit-level syntax validation test. |
| 4 | +
|
| 5 | +use std::collections::HashSet; |
| 6 | +use std::fs; |
| 7 | +use std::path::Path; |
| 8 | +use std::process::Command; |
| 9 | + |
| 10 | +fn create_project_with_macro_fns(dir: &Path) { |
| 11 | + fs::create_dir_all(dir.join("src")).unwrap(); |
| 12 | + |
| 13 | + fs::write( |
| 14 | + dir.join("Cargo.toml"), |
| 15 | + r#"[package] |
| 16 | +name = "macro-fns" |
| 17 | +version = "0.1.0" |
| 18 | +edition = "2024" |
| 19 | +
|
| 20 | +[[bin]] |
| 21 | +name = "macro-fns" |
| 22 | +path = "src/main.rs" |
| 23 | +"#, |
| 24 | + ) |
| 25 | + .unwrap(); |
| 26 | + |
| 27 | + fs::write( |
| 28 | + dir.join("src").join("main.rs"), |
| 29 | + r#"macro_rules! make_handler { |
| 30 | + ($name:ident) => { |
| 31 | + fn $name() -> u64 { |
| 32 | + let mut sum = 0u64; |
| 33 | + for i in 0..100 { |
| 34 | + sum += i; |
| 35 | + } |
| 36 | + sum |
| 37 | + } |
| 38 | + }; |
| 39 | +} |
| 40 | +
|
| 41 | +macro_rules! make_pair { |
| 42 | + ($a:ident, $b:ident) => { |
| 43 | + fn $a() -> u64 { 42 } |
| 44 | + fn $b() -> u64 { 99 } |
| 45 | + }; |
| 46 | +} |
| 47 | +
|
| 48 | +make_handler!(compute); |
| 49 | +make_pair!(alpha, beta); |
| 50 | +
|
| 51 | +fn main() { |
| 52 | + let a = compute(); |
| 53 | + let b = alpha(); |
| 54 | + let c = beta(); |
| 55 | + println!("results: {a} {b} {c}"); |
| 56 | +} |
| 57 | +"#, |
| 58 | + ) |
| 59 | + .unwrap(); |
| 60 | +} |
| 61 | + |
| 62 | +#[test] |
| 63 | +fn macro_generated_fns_appear_in_output() { |
| 64 | + let tmp = tempfile::tempdir().unwrap(); |
| 65 | + let project_dir = tmp.path().join("macro-fns"); |
| 66 | + create_project_with_macro_fns(&project_dir); |
| 67 | + |
| 68 | + let piano_bin = env!("CARGO_BIN_EXE_piano"); |
| 69 | + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); |
| 70 | + let runtime_path = manifest_dir.join("piano-runtime"); |
| 71 | + |
| 72 | + // Build with no target filter -- activates instrument_macros = true. |
| 73 | + let output = Command::new(piano_bin) |
| 74 | + .args(["build", "--project"]) |
| 75 | + .arg(&project_dir) |
| 76 | + .arg("--runtime-path") |
| 77 | + .arg(&runtime_path) |
| 78 | + .output() |
| 79 | + .expect("failed to run piano build"); |
| 80 | + |
| 81 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 82 | + let stdout = String::from_utf8_lossy(&output.stdout); |
| 83 | + |
| 84 | + assert!( |
| 85 | + output.status.success(), |
| 86 | + "piano build failed:\nstderr: {stderr}\nstdout: {stdout}" |
| 87 | + ); |
| 88 | + |
| 89 | + // Run the instrumented binary. |
| 90 | + let runs_dir = tmp.path().join("runs"); |
| 91 | + fs::create_dir_all(&runs_dir).unwrap(); |
| 92 | + |
| 93 | + let binary_path = stdout.trim(); |
| 94 | + let run_output = Command::new(binary_path) |
| 95 | + .env("PIANO_RUNS_DIR", &runs_dir) |
| 96 | + .output() |
| 97 | + .expect("failed to run instrumented binary"); |
| 98 | + |
| 99 | + assert!( |
| 100 | + run_output.status.success(), |
| 101 | + "instrumented binary failed:\n{}", |
| 102 | + String::from_utf8_lossy(&run_output.stderr) |
| 103 | + ); |
| 104 | + |
| 105 | + // Program correctness: compute() = 4950, alpha() = 42, beta() = 99. |
| 106 | + let program_stdout = String::from_utf8_lossy(&run_output.stdout); |
| 107 | + assert!( |
| 108 | + program_stdout.contains("results: 4950 42 99"), |
| 109 | + "program should produce correct output, got: {program_stdout}" |
| 110 | + ); |
| 111 | + |
| 112 | + // Verify run file contains the macro-generated function names. |
| 113 | + let run_files: Vec<_> = fs::read_dir(&runs_dir) |
| 114 | + .unwrap() |
| 115 | + .filter_map(|e| e.ok()) |
| 116 | + .filter(|e| { |
| 117 | + e.path() |
| 118 | + .extension() |
| 119 | + .is_some_and(|ext| ext == "json" || ext == "ndjson") |
| 120 | + }) |
| 121 | + .collect(); |
| 122 | + |
| 123 | + assert!(!run_files.is_empty(), "expected at least one run file"); |
| 124 | + |
| 125 | + let content = fs::read_to_string(run_files[0].path()).unwrap(); |
| 126 | + |
| 127 | + // Metavar-generated functions should appear by their expanded names. |
| 128 | + assert!( |
| 129 | + content.contains("\"compute\""), |
| 130 | + "output should contain macro-generated function 'compute'" |
| 131 | + ); |
| 132 | + assert!( |
| 133 | + content.contains("\"alpha\""), |
| 134 | + "output should contain macro-generated function 'alpha'" |
| 135 | + ); |
| 136 | + assert!( |
| 137 | + content.contains("\"beta\""), |
| 138 | + "output should contain macro-generated function 'beta'" |
| 139 | + ); |
| 140 | + |
| 141 | + // main should also be instrumented (no target filter = instrument all). |
| 142 | + assert!(content.contains("\"main\""), "output should contain 'main'"); |
| 143 | +} |
| 144 | + |
| 145 | +#[test] |
| 146 | +fn instrumented_macro_output_is_valid_syntax() { |
| 147 | + // Use instrument_source directly and verify the output parses as valid Rust. |
| 148 | + let source = r#" |
| 149 | +macro_rules! make_handler { |
| 150 | + ($name:ident) => { |
| 151 | + fn $name() -> u64 { |
| 152 | + let mut sum = 0u64; |
| 153 | + for i in 0..100 { |
| 154 | + sum += i; |
| 155 | + } |
| 156 | + sum |
| 157 | + } |
| 158 | + }; |
| 159 | +} |
| 160 | +
|
| 161 | +macro_rules! make_pair { |
| 162 | + ($a:ident, $b:ident) => { |
| 163 | + fn $a() -> u64 { 42 } |
| 164 | + fn $b() -> u64 { 99 } |
| 165 | + }; |
| 166 | +} |
| 167 | +
|
| 168 | +make_handler!(compute); |
| 169 | +make_pair!(alpha, beta); |
| 170 | +
|
| 171 | +fn main() { |
| 172 | + let a = compute(); |
| 173 | + let b = alpha(); |
| 174 | + let c = beta(); |
| 175 | + println!("results: {a} {b} {c}"); |
| 176 | +} |
| 177 | +"#; |
| 178 | + |
| 179 | + let targets: HashSet<String> = HashSet::new(); |
| 180 | + let result = piano::rewrite::instrument_source(source, &targets, true) |
| 181 | + .expect("instrument_source should succeed"); |
| 182 | + |
| 183 | + // The instrumented source must parse as valid Rust. |
| 184 | + let parsed: Result<syn::File, _> = syn::parse_str(&result.source); |
| 185 | + assert!( |
| 186 | + parsed.is_ok(), |
| 187 | + "instrumented macro output should be valid Rust syntax. Error: {:?}\nSource:\n{}", |
| 188 | + parsed.err(), |
| 189 | + result.source |
| 190 | + ); |
| 191 | + |
| 192 | + // Verify the guards were actually injected. |
| 193 | + assert!( |
| 194 | + result.source.contains("piano_runtime::enter"), |
| 195 | + "instrumented source should contain piano_runtime::enter guards.\nSource:\n{}", |
| 196 | + result.source |
| 197 | + ); |
| 198 | +} |
0 commit comments