runner/
runner.rs

1mod code;
2use code::output::{Output, Status};
3use proptest::prelude::TestCaseError;
4use proptest::test_runner::{Config, TestError, TestRunner};
5use std::cell::RefCell;
6use std::io::{BufRead, BufReader, Write};
7use std::process::Stdio;
8use std::{fs, path::PathBuf};
9
10const START_TAG: &str = ";; ELPROP_START:";
11const END_TAG: &str = "\n;; ELPROP_END\n";
12
13fn main() {
14    let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
15    let workspace_root = crate_root.parent().unwrap();
16    let target = workspace_root.join("target/elprop");
17
18    let json = target.join("functions.json");
19    // read the file to a string
20    let json_string = fs::read_to_string(json).expect("Unable to read file");
21
22    let config: code::data::Config =
23        serde_json::from_str(&json_string).expect("Unable to deserialize json");
24
25    let mut runner = TestRunner::new(Config {
26        cases: config.test_count,
27        failure_persistence: None,
28        ..Config::default()
29    });
30
31    let cmd = crate_root.parent().unwrap().join("target/debug/rune");
32    #[expect(clippy::zombie_processes)]
33    let mut child = std::process::Command::new(cmd)
34        .arg("--eval-stdin")
35        .stdin(Stdio::piped())
36        .stdout(Stdio::piped())
37        .spawn()
38        .expect("Failed to start rune");
39
40    let rune_stdin = RefCell::new(child.stdin.take().unwrap());
41    let rune_stdout = RefCell::new(child.stdout.take().unwrap());
42    let rune_panicked = RefCell::new(false);
43    let master_count = RefCell::new(0);
44
45    let outputs = RefCell::new(Vec::new());
46    let regex = regex::Regex::new("^\\((wrong-[a-zA-Z0-9-]*|[a-zA-Z0-9-]*error) ").unwrap();
47
48    for func in config.functions {
49        let name = func.name.clone();
50        let func_iter_count = RefCell::new(0);
51
52        let result = runner.run(&func.strategy(), |input| {
53            if *rune_panicked.borrow() {
54                return Err(TestCaseError::Reject("Rune panicked".into()));
55            }
56            let body = code::data::print_args(&input);
57            // send to emacs
58            println!(";; sending to Emacs");
59            let test_str = format!(";; ELPROP_START\n({name} {body})\n;; ELPROP_END");
60            println!("{test_str}");
61            // send to rune
62            println!(";; sending to rune");
63            match writeln!(rune_stdin.borrow_mut(), "{test_str}") {
64                Ok(()) => (),
65                Err(e) => {
66                    *rune_panicked.borrow_mut() = true;
67                    return Err(TestCaseError::Reject(format!("Rune panicked {e}").into()));
68                }
69            }
70
71            let mut reader = BufReader::new(std::io::stdin());
72            println!(";; reading from Emacs");
73            let emacs_output =
74                process_eval_result("Emacs", *master_count.borrow(), &mut reader, |text| {
75                    regex.is_match(text)
76                });
77
78            let mut rune_stdout = rune_stdout.borrow_mut();
79            let mut reader = BufReader::new(&mut *rune_stdout);
80            println!(";; reading from Rune");
81            let rune_output =
82                process_eval_result("Rune", *master_count.borrow(), &mut reader, |text| {
83                    text.starts_with("Error: ")
84                });
85            println!(";; done");
86
87            *master_count.borrow_mut() += 1;
88            *func_iter_count.borrow_mut() += 1;
89
90            match (emacs_output, rune_output) {
91                (Ok(e), Ok(r)) if e == r => Ok(()),
92                (Err(_), Err(_)) => Ok(()),
93                (Ok(e) | Err(e), Ok(r)) | (Ok(e), Err(r)) => {
94                    println!("\"Emacs: '{e}', Rune: '{r}'\"");
95                    Err(TestCaseError::Fail(format!("Emacs: {e}, Rune: {r}").into()))
96                }
97            }
98        });
99
100        // send the output of "result" to a file
101        // open the file in write mode
102
103        println!(";; sending output");
104        let status = match result {
105            Err(TestError::Fail(reason, value)) => Status::Fail(reason.to_string(), value),
106            Err(TestError::Abort(reason)) => Status::Abort(reason.to_string()),
107            Ok(()) => Status::Pass,
108        };
109        let output = Output { function: name, count: *func_iter_count.borrow(), status };
110        outputs.borrow_mut().push(output);
111    }
112
113    let _ = child.kill();
114    let json = serde_json::to_string(&*outputs.borrow()).expect("Malformed Output JSON");
115    let output_file = target.join("output.json");
116    fs::write(output_file, json).unwrap();
117    println!(";; exit process");
118}
119
120fn process_eval_result(
121    name: &str,
122    master_count: usize,
123    reader: &mut impl BufRead,
124    test_fail: impl Fn(&str) -> bool,
125) -> Result<String, String> {
126    let mut line = String::new();
127    reader.read_line(&mut line).unwrap();
128    if line.contains("thread 'main' panicked") {
129        return Err("Rune panicked".to_owned());
130    }
131    let count = line.strip_prefix(START_TAG).unwrap().trim().parse::<usize>().unwrap();
132    assert_eq!(
133        master_count, count,
134        "Count from {name} was off. actual {count}, expected {master_count}",
135    );
136    line.clear();
137    while !line.contains(END_TAG) {
138        reader.read_line(&mut line).unwrap();
139    }
140    let text = line.strip_suffix(END_TAG).unwrap().trim().to_string();
141    if test_fail(&text) { Err(text) } else { Ok(text) }
142}