1mod code;
2use clap::Parser;
3use code::data::{Config, Function};
4use code::output::{Output, Status};
5use std::path::Path;
6use std::process::ExitCode;
7use std::{fs, path::PathBuf};
8
9#[derive(Parser)]
10#[command(author, version, about, long_about = None)]
11struct Cli {
12 pattern: String,
14 #[arg(short, long)]
16 test_count: Option<u32>,
17}
18
19fn main() -> ExitCode {
20 let cli = Cli::parse();
21
22 let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
23 let workspace_root = crate_root.parent().unwrap();
24 let rust_src = workspace_root.join("src");
26 eprintln!("Generating Functions...");
27 let regex = regex::Regex::new(&cli.pattern).unwrap();
28 let functions = get_all_functions(&rust_src)
29 .into_iter()
30 .filter(|x| {
31 let rust_name = x.name.replace(['-'], "_");
32 regex.is_match(&x.name) || regex.is_match(&rust_name)
33 })
34 .collect::<Vec<_>>();
35
36 let config = Config { test_count: cli.test_count.unwrap_or(200), functions: functions.clone() };
37
38 let json = serde_json::to_string(&config);
39 let elprop_target = workspace_root.join("target/elprop");
40 fs::create_dir_all(&elprop_target).unwrap();
41 let function_file = elprop_target.join("functions.json");
42 fs::write(function_file, json.expect("Malformed JSON")).unwrap();
43 #[allow(unused_variables)]
44 let emacs_cmd_file = crate_root.join("src/elprop.el");
45 let runner = workspace_root.join("target/debug/runner");
46 eprintln!("Launching Proptest...");
47 let lisp_cmd = format!("(load-file \"{}\")", emacs_cmd_file.to_str().unwrap());
48 let child = std::process::Command::new("emacs")
49 .args(["-Q", "--batch", "--eval", &lisp_cmd])
50 .env("ELPROP_RUNNER", runner)
51 .output()
52 .expect("Failed to run emacs");
53 let output = String::from_utf8_lossy(&child.stdout);
54 if let Some(panic_idx) = output.find("thread 'main' panicked") {
56 let start_text = ";; ELPROP_START";
58 let end_text = ";; ELPROP_END";
59 let start = match output.find(start_text) {
60 Some(i) => i + start_text.len(),
61 None => 0,
62 };
63 let end = output.find(end_text).unwrap_or(output.len());
64 let source = &output[start..end];
65
66 let backtrace_end = match output[panic_idx..].find(";; ") {
67 Some(i) => i + panic_idx,
68 None => output.len(),
69 };
70 let backtrace = &output[panic_idx..backtrace_end];
71
72 println!("====================");
73 println!("Status: Failed (Rune Panicked)");
74 println!("Failing Input: {source}");
75 println!("{backtrace}");
76 return ExitCode::FAILURE;
77 }
78
79 println!("{output}");
80
81 let code = child.status.code().unwrap();
82 if code != 0 {
83 eprintln!("Emacs exited with code: {code}");
84 return ExitCode::from(code as u8);
85 }
86
87 let output_file = elprop_target.join("output.json");
88 let json_string = fs::read_to_string(output_file).expect("Unable to read output file");
89 let outputs: Vec<Output> =
90 serde_json::from_str(&json_string).expect("Unable to deserialize Output json");
91
92 let mut passed = true;
93 let count = outputs.len();
94 for output in outputs {
95 println!("====================");
96 let func = output.function;
97 println!("Testing: {func} [{}]", output.count);
98 match output.status {
99 Status::Fail(reason, args) => {
100 passed = false;
101 println!("Status: Failed");
102
103 let body = code::data::print_args(&args);
104 println!("Input: ({func} {body})");
105 println!("Output: {reason}");
106 }
107 Status::Abort(reason) => {
108 passed = false;
109 println!("{func}: Aborted\nReason: {reason}");
110 }
111 Status::Pass => println!("{func}: Passed"),
112 }
113 println!();
114 }
115 if count == 0 {
116 println!("No tests run");
117 ExitCode::SUCCESS
118 } else if passed {
119 println!("All tests passed");
120 ExitCode::SUCCESS
121 } else {
122 ExitCode::FAILURE
123 }
124}
125
126fn get_all_functions(pathbuf: &Path) -> Vec<Function> {
127 let mut functions = Vec::new();
128 for entry in fs::read_dir(pathbuf).unwrap() {
129 let path = entry.unwrap().path();
130 if path.extension().is_some_and(|ex| ex == "rs") {
131 let contents = fs::read_to_string(&path).unwrap();
132 let names = get_fn_signatures(&contents);
133 functions.extend(names);
134 }
135 }
136 functions
137}
138
139fn get_fn_signatures(string: &str) -> Vec<Function> {
140 syn::parse_file(string)
141 .unwrap()
142 .items
143 .iter()
144 .filter_map(|x| match x {
145 syn::Item::Fn(x) => Some(x),
146 _ => None,
147 })
148 .filter(is_defun)
149 .filter_map(|x| Function::from_item(x).ok())
150 .collect()
151}
152
153#[expect(clippy::trivially_copy_pass_by_ref)]
154fn is_defun(func: &&syn::ItemFn) -> bool {
155 use syn::Meta;
156 use syn::MetaList;
157 for attr in &func.attrs {
158 match &attr.meta {
159 Meta::Path(path) | Meta::List(MetaList { path, .. })
160 if path.get_ident().unwrap() == "defun" =>
161 {
162 return true;
163 }
164 _ => {}
165 }
166 }
167 false
168}