Skip to content

Commit 8347499

Browse files
committed
test: add proper to_bash roundtrip test for snapshots
The new test_snapshots_to_bash_roundtrip test verifies that all snapshot scripts can be parsed, converted to bash via to_bash, and re-parsed to produce an equivalent AST (ignoring line numbers). Known failures (30_multiple_heredocs.sh, 49_heredoc_variations.sh) are skipped due to heredoc content placement limitations.
1 parent 22b500b commit 8347499

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

‎tests/snapshots.rs‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,111 @@ fn test_snapshots_roundtrip() {
122122
);
123123
}
124124
}
125+
126+
/// Test full roundtrip: parse -> `to_bash` -> parse -> compare AST
127+
///
128+
/// This verifies that `to_bash` produces valid bash that parses to an equivalent AST.
129+
/// Line numbers are ignored since regenerated code has different formatting.
130+
#[test]
131+
fn test_snapshots_to_bash_roundtrip() {
132+
use bash_ast::{parse, to_bash};
133+
134+
setup();
135+
136+
let snapshot_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
137+
138+
// Known failures due to heredoc limitations
139+
let known_failures = ["30_multiple_heredocs.sh", "49_heredoc_variations.sh"];
140+
141+
let mut scripts: Vec<_> = fs::read_dir(&snapshot_dir)
142+
.expect("Failed to read snapshots directory")
143+
.filter_map(std::result::Result::ok)
144+
.map(|e| e.path())
145+
.filter(|p| p.extension().is_some_and(|ext| ext == "sh"))
146+
.collect();
147+
148+
scripts.sort();
149+
150+
let mut failures = Vec::new();
151+
152+
for script_path in &scripts {
153+
let script_name = script_path.file_name().unwrap().to_string_lossy();
154+
155+
// Skip known failures
156+
if known_failures.iter().any(|&f| script_name == f) {
157+
continue;
158+
}
159+
160+
let script = fs::read_to_string(script_path)
161+
.unwrap_or_else(|e| panic!("Failed to read {script_path:?}: {e}"));
162+
163+
// Parse original
164+
let ast1 = match parse(&script) {
165+
Ok(ast) => ast,
166+
Err(e) => {
167+
failures.push(format!("{script_name}: failed to parse original: {e}"));
168+
continue;
169+
}
170+
};
171+
172+
// Convert to bash
173+
let regenerated = to_bash(&ast1);
174+
175+
// Parse regenerated
176+
let ast2 = match parse(&regenerated) {
177+
Ok(ast) => ast,
178+
Err(e) => {
179+
failures.push(format!(
180+
"{script_name}: failed to parse regenerated script: {e}\nRegenerated:\n{regenerated}"
181+
));
182+
continue;
183+
}
184+
};
185+
186+
// Compare ASTs (ignoring line numbers)
187+
let json1 = serde_json::to_string(&ast1).unwrap();
188+
let json2 = serde_json::to_string(&ast2).unwrap();
189+
190+
let json1_normalized = normalize_json_for_comparison(&json1);
191+
let json2_normalized = normalize_json_for_comparison(&json2);
192+
193+
if json1_normalized != json2_normalized {
194+
failures.push(format!(
195+
"{script_name}: AST mismatch after roundtrip\nOriginal AST:\n{json1}\nRegenerated script:\n{regenerated}\nRegenerated AST:\n{json2}"
196+
));
197+
}
198+
}
199+
200+
assert!(
201+
failures.is_empty(),
202+
"\n{} roundtrip test(s) failed:\n\n{}",
203+
failures.len(),
204+
failures.join("\n\n---\n\n")
205+
);
206+
}
207+
208+
/// Remove line numbers from JSON for comparison
209+
fn normalize_json_for_comparison(json: &str) -> String {
210+
// Parse and re-serialize without line numbers
211+
let mut value: serde_json::Value = serde_json::from_str(json).unwrap();
212+
remove_line_numbers(&mut value);
213+
serde_json::to_string(&value).unwrap()
214+
}
215+
216+
/// Recursively remove "line" fields from JSON value
217+
fn remove_line_numbers(value: &mut serde_json::Value) {
218+
match value {
219+
serde_json::Value::Object(map) => {
220+
map.remove("line");
221+
for v in map.values_mut() {
222+
remove_line_numbers(v);
223+
}
224+
}
225+
serde_json::Value::Array(arr) => {
226+
for v in arr {
227+
remove_line_numbers(v);
228+
}
229+
}
230+
_ => {}
231+
}
232+
}

0 commit comments

Comments
 (0)