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
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 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}