rustpython_unparser/
lib.rs1pub mod unparser;
2mod utils;
3pub use crate::unparser::Unparser;
4#[cfg(feature = "transformer")]
5pub mod transformer;
6#[cfg(test)]
7mod tests {
8 use super::*;
9 use pretty_assertions::assert_eq;
10 use rand::Rng;
11 use rustpython_ast::text_size::TextRange;
12 use rustpython_ast::Fold;
13 use rustpython_ast::TextSize;
14 use rustpython_parser::ast::Suite;
15 use rustpython_parser::Parse;
16
17 use std::fs;
18 use std::io;
19 use std::path::Path;
20 use std::process::Command;
21
22 struct RangesEraser {}
23
24 impl Fold<TextRange> for RangesEraser {
25 type TargetU = TextRange;
26
27 type Error = std::convert::Infallible;
28
29 type UserContext = TextRange;
30
31 fn will_map_user(&mut self, _user: &TextRange) -> Self::UserContext {
32 TextRange::new(TextSize::new(0), TextSize::new(0))
33 }
34
35 fn map_user(
36 &mut self,
37 _user: TextRange,
38 start: Self::UserContext,
39 ) -> Result<Self::TargetU, Self::Error> {
40 Ok(start)
41 }
42 }
43
44 fn run_tests_on_folders(source_folder: &str, results_folder: &str) -> io::Result<()> {
45 for entry in fs::read_dir(results_folder)? {
46 let entry = entry?;
47
48 let entry_path = entry.path();
49 if entry_path.is_file()
50 && entry_path.file_name().is_some_and(|name| {
51 name.to_str()
52 .is_some_and(|inner_name| inner_name.ends_with(".py"))
53 })
54 {
55 fs::remove_file(entry_path)?;
56 }
57 }
58
59 for entry in fs::read_dir(source_folder)? {
60 let entry = entry?;
61
62 let entry_path = entry.path();
63
64 if entry_path.is_file()
65 && entry_path.file_name().is_some_and(|name| {
66 name.to_str()
67 .is_some_and(|inner_name| inner_name.ends_with(".py"))
68 })
69 {
70 let file_content = fs::read_to_string(&entry_path)?;
71 let entry_path_str = entry_path.to_str().unwrap();
72 let mut unparser = Unparser::new();
73 let stmts = Suite::parse(&file_content, entry_path_str).unwrap();
74 for stmt in &stmts {
75 unparser.unparse_stmt(stmt);
76 }
77 let new_source = unparser.source;
78 let old_file_name = entry_path.file_name().unwrap().to_str().unwrap();
79 let new_file_name = old_file_name.replace(".py", "_unparsed.py");
80 let new_entry_path_str = format!("{}/{}", results_folder, new_file_name);
81 let new_entry_path = Path::new(&new_entry_path_str);
82 fs::write(&new_entry_path, &new_source)?;
83 let new_stmts =
84 Suite::parse(&new_source, new_entry_path.to_str().unwrap()).unwrap();
85 let mut eraser = RangesEraser {};
87 let mut erased_new_stmts = Vec::new();
88 for stmt in &new_stmts {
89 erased_new_stmts.push(eraser.fold_stmt(stmt.to_owned()).unwrap());
90 }
91
92 let mut erased_stmts = Vec::new();
93 for stmt in &stmts {
94 erased_stmts.push(eraser.fold_stmt(stmt.to_owned()).unwrap());
95 }
96
97 for (stmt, new_stmt) in erased_stmts.iter().zip(erased_new_stmts.iter()) {
98 assert_eq!(stmt, new_stmt)
99 }
100 }
101 }
102 Ok(())
103 }
104
105 #[test]
106 fn test_predefined_files() -> io::Result<()> {
107 run_tests_on_folders("./test_files", "./test_files_unparsed")
108 }
109 #[test]
110 #[ignore = "Fuzzy tests are unstable and should only be used to explore new test cases"]
111 fn test_fuzzy_files() -> io::Result<()> {
112 let seed: u64 = rand::rng().random();
113
114 for i in 0..10 {
115 let file_name = format!("./fuzzy_test_files/fuzzy_test{}.py", i);
116
117 let file_content = Command::new(".venv/bin/python")
118 .arg("-m")
119 .arg("pysource_codegen")
120 .arg("--seed")
121 .arg(&seed.to_string())
122 .output()
123 .expect("failed to execute process");
124 fs::write(&file_name, &file_content.stdout)?;
125 }
126
127 run_tests_on_folders("./fuzzy_test_files", "./fuzzy_test_files_unparsed")?;
128
129 Ok(())
130 }
131}