Skip to main content

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