clashlib/
solution.rs

1mod test_result;
2
3use std::io::Write;
4use std::process::Command;
5use std::time::Duration;
6
7use test_result::CommandExit;
8pub use test_result::TestResult;
9use wait_timeout::ChildExt;
10
11use crate::clash::Testcase;
12
13/// Run a command against testcases one at a time.
14///
15/// # Examples
16///
17/// ```
18/// use clashlib::clash::Testcase;
19/// use clashlib::solution::lazy_run;
20///
21/// let testcases = [
22///     Testcase {
23///         index: 1,
24///         title: String::from("Test #1"),
25///         test_in: String::from("hey"),
26///         test_out: String::from("hey"),
27///         is_validator: false,
28///     }
29/// ];
30/// let mut command = std::process::Command::new("cat");
31/// let timeout = std::time::Duration::from_secs(5);
32///
33/// for (testcase, test_result) in lazy_run(&testcases, &mut command, &timeout) {
34///     assert_eq!(testcase.title, "Test #1");
35///     assert!(test_result.is_success());
36/// }
37/// ```
38pub fn lazy_run<'a>(
39    testcases: impl IntoIterator<Item = &'a Testcase>,
40    run_command: &'a mut Command,
41    timeout: &'a Duration,
42) -> impl IntoIterator<Item = (&'a Testcase, TestResult)> {
43    testcases.into_iter().map(|test| {
44        let result = run_testcase(test, run_command, timeout);
45        (test, result)
46    })
47}
48
49/// Run a command against a single testcase.
50pub fn run_testcase(testcase: &Testcase, run_command: &mut Command, timeout: &Duration) -> TestResult {
51    let mut run = match run_command
52        .stdin(std::process::Stdio::piped())
53        .stdout(std::process::Stdio::piped())
54        .stderr(std::process::Stdio::piped())
55        .spawn()
56    {
57        Ok(run) => run,
58        Err(error) => {
59            let program = run_command.get_program().to_str().unwrap_or("Unable to run command");
60            let error_msg = format!("{}: {}", program, error);
61            return TestResult::UnableToRun { error_msg }
62        }
63    };
64
65    run.stdin
66        .as_mut()
67        .expect("STDIN of child process should be captured")
68        .write_all(testcase.test_in.as_bytes())
69        .expect("STDIN of child process should be writable");
70
71    let timed_out = run
72        .wait_timeout(*timeout)
73        .expect("Process should be able to wait for execution")
74        .is_none();
75
76    if timed_out {
77        run.kill().expect("Process should have been killed");
78    }
79
80    let output = run.wait_with_output().expect("Process should allow waiting for its execution");
81
82    let exit_status = if timed_out {
83        CommandExit::Timeout
84    } else if output.status.success() {
85        CommandExit::Ok
86    } else {
87        CommandExit::Error
88    };
89    TestResult::from_output(&testcase.test_out, output.stdout, output.stderr, exit_status)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_passing_solution() {
98        let clash = crate::test_helper::sample_puzzle("stub_and_solution_tester").unwrap();
99        let mut run_cmd = Command::new("tr");
100        run_cmd.arg("X");
101        run_cmd.arg("b");
102        let timeout = Duration::from_secs(1);
103        assert!(lazy_run(clash.testcases(), &mut run_cmd, &timeout)
104            .into_iter()
105            .all(|(_, test_result)| test_result.is_success()))
106    }
107
108    #[test]
109    fn test_failing_solution() {
110        let clash = crate::test_helper::sample_puzzle("stub_and_solution_tester").unwrap();
111        let timeout = Duration::from_secs(1);
112        let mut run_cmd = Command::new("cat");
113        assert!(lazy_run(clash.testcases(), &mut run_cmd, &timeout)
114            .into_iter()
115            .all(|(_, test_result)| !test_result.is_success()))
116    }
117}