rustpython_unparser/
lib.rs

1pub 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                // erase range information
86                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}