Skip to content

Commit 5768d20

Browse files
test(e2e): add macro_rules! instrumentation integration test
full pipeline test: synthetic project with macro_rules! -> piano build -> run -> verify function names in NDJSON output. separate syntax validation test confirms instrumented macro output parses as valid Rust.
1 parent b589d8a commit 5768d20

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed

tests/macro_rules.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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

Comments
 (0)