elprop/
elprop.rs

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    /// The pattern to match rune functions against
13    pattern: String,
14    /// The number of tests to run per function (default 100)
15    #[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    // go to the source directory
25    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    // find the start of the text mapbacktrace if it exists in output
55    if let Some(panic_idx) = output.find("thread 'main' panicked") {
56        // get the text between START and END tags
57        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}