Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
.unwrap()
1use std::path::PathBuf;23fn get_fixture_json_dir() -> PathBuf {4 if let Ok(dir) = std::env::var("FIXTURE_JSON_DIR") {5 return PathBuf::from(dir);6 }7 // Default: fixtures checked in alongside the test8 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")9}1011/// Recursively sort all keys in a JSON value for order-independent comparison.12fn normalize_json(value: &serde_json::Value) -> serde_json::Value {13 match value {14 serde_json::Value::Object(map) => {15 let mut sorted: Vec<(String, serde_json::Value)> = map16 .iter()17 .map(|(k, v)| (k.clone(), normalize_json(v)))18 .collect();19 sorted.sort_by(|a, b| a.0.cmp(&b.0));20 serde_json::Value::Object(sorted.into_iter().collect())21 }22 serde_json::Value::Array(arr) => {23 serde_json::Value::Array(arr.iter().map(normalize_json).collect())24 }25 // Normalize numbers: f64 values like 1.0 should compare equal to integer 126 serde_json::Value::Number(n) => {27 if let Some(f) = n.as_f64() {28 if f.fract() == 0.0 && f.is_finite() && f.abs() < (i64::MAX as f64) {29 serde_json::Value::Number(serde_json::Number::from(f as i64))30 } else {31 value.clone()32 }33 } else {34 value.clone()35 }36 }37 other => other.clone(),38 }39}4041fn compute_diff(original: &str, round_tripped: &str) -> String {42 use similar::ChangeTag;43 use similar::TextDiff;4445 let diff = TextDiff::from_lines(original, round_tripped);46 let mut output = String::new();47 let mut lines_written = 0;48 const MAX_DIFF_LINES: usize = 50;4950 for change in diff.iter_all_changes() {51 if lines_written >= MAX_DIFF_LINES {52 output.push_str("... (diff truncated)\n");53 break;54 }55 let sign = match change.tag() {56 ChangeTag::Delete => "-",57 ChangeTag::Insert => "+",58 ChangeTag::Equal => continue,59 };60 output.push_str(&format!("{sign} {change}"));61 lines_written += 1;62 }6364 output65}6667#[test]68fn round_trip_all_fixtures() {69 let json_dir = get_fixture_json_dir();7071 let mut failures: Vec<(String, String)> = Vec::new();72 let mut total = 0;73 let mut passed = 0;7475 // Fixtures with known issues that can't be fixed in the AST crate:76 // - lone-surrogate-string-values: contains lone Unicode surrogates (\uD800)77 // that serde_json rejects during deserialization.78 let known_failures: &[&str] = &["lone-surrogate-string-values"];7980 for entry in walkdir::WalkDir::new(&json_dir)81 .into_iter()82 .filter_map(|e| e.ok())83 .filter(|e| {84 e.path().extension().is_some_and(|ext| ext == "json")85 && !e.path().to_string_lossy().ends_with(".scope.json")86 && !e.path().to_string_lossy().ends_with(".renamed.json")87 })88 {89 let fixture_name = entry90 .path()91 .strip_prefix(&json_dir)92 .unwrap()93 .display()94 .to_string();95 let original_json = std::fs::read_to_string(entry.path()).unwrap();9697 if known_failures.iter().any(|kf| fixture_name.contains(kf)) {98 continue;99 }100101 total += 1;102103 // Deserialize into our Rust types104 let ast: react_compiler_ast::File = match serde_json::from_str(&original_json) {105 Ok(ast) => ast,106 Err(e) => {107 failures.push((fixture_name, format!("Deserialization error: {e}")));108 continue;109 }110 };111112 // Re-serialize back to JSON113 let round_tripped = serde_json::to_string_pretty(&ast).unwrap();114115 // Normalize and compare116 let original_value: serde_json::Value = serde_json::from_str(&original_json).unwrap();117 let round_tripped_value: serde_json::Value = serde_json::from_str(&round_tripped).unwrap();118119 let original_normalized = normalize_json(&original_value);120 let round_tripped_normalized = normalize_json(&round_tripped_value);121122 if original_normalized != round_tripped_normalized {123 let orig_str = serde_json::to_string_pretty(&original_normalized).unwrap();124 let rt_str = serde_json::to_string_pretty(&round_tripped_normalized).unwrap();125 let diff = compute_diff(&orig_str, &rt_str);126 failures.push((fixture_name, diff));127 } else {128 passed += 1;129 }130 }131132 println!("\n{passed}/{total} fixtures passed round-trip");133134 if !failures.is_empty() {135 let show_count = failures.len().min(5);136 let mut msg = format!(137 "\n{} of {total} fixtures failed round-trip (showing first {show_count}):\n\n",138 failures.len()139 );140 for (name, diff) in failures.iter().take(show_count) {141 msg.push_str(&format!("--- {name} ---\n{diff}\n\n"));142 }143 if failures.len() > show_count {144 msg.push_str(&format!(145 "... and {} more failures\n",146 failures.len() - show_count147 ));148 }149 panic!("{msg}");150 }151}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.