summaryrefslogtreecommitdiff
path: root/rust/src/tester.rs
blob: eb66d4e99ae083e0b87188fbb0f7f17698009a05 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
use serde::{Deserialize, Serialize};
use std::{
    fs::{self, read_to_string, File},
    path::PathBuf,
    process::{Command, Stdio},
    time::Instant,
};

pub fn main() {
    let mut args: Vec<String> = std::env::args().collect();
    // name of the process to run
    let proc = args.remove(1);
    if args.len() > 1 {
        let run = traverse(&args)
            .and_then(|paths| run_test(&proc, &paths))
            .expect("Failed to traverse directory");
        println!("{}", serde_json::to_string_pretty(&run).unwrap());
    } else {
        println!(
            r#"Usage: tester [options] <directory>+

Options:
    -p, --process   The process to run. If the given process is not a
                    an absolute path, it will be resolved against the
                    PATH environment variable.
    -j, --json      Output the results in JSON format (default: false)
    -h, --help      Display this help message
    -v, --version   Display the version of the tester
"#
        );
    }
}

fn traverse(args: &[String]) -> Result<Vec<PathBuf>, String> {
    let mut files: Vec<PathBuf> = Vec::new();
    for arg in args.iter().skip(1) {
        let entries = fs::read_dir(arg).map_err(|e| e.to_string())?;
        for entry in entries {
            let dir = entry.map_err(|e| e.to_string())?;
            let f = dir.metadata().map_err(|e| e.to_string())?;
            if f.is_dir() {
                files.push(dir.path());
            }
        }
    }
    Ok(files)
}

#[derive(Debug, Serialize, Deserialize)]
pub enum TestResult {
    TestSucceeded,
    TestFailed(String, String),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TestRun {
    file: String,
    test_result: TestResult,
    duration: u128,
}

fn run_test(proc: &str, files: &Vec<PathBuf>) -> Result<Vec<TestRun>, String> {
    let mut result = Vec::new();
    for file in files {
        let mut inp = file.clone();
        let mut outp = file.clone();
        inp.push("input");
        outp.push("output");
        let (test_result, duration) = run_test_case(proc, &inp, &outp)?;
        result.push(TestRun {
            file: inp.as_path().to_str().unwrap().to_string(),
            test_result,
            duration,
        });
    }
    Ok(result)
}

fn run_test_case(
    proc: &str,
    inp: &std::path::PathBuf,
    outp: &std::path::PathBuf,
) -> Result<(TestResult, u128), String> {
    let input = File::open(inp).map_err(|e| e.to_string())?;
    let expected = read_to_string(outp).map_err(|e| e.to_string())?;
    let start = Instant::now();

    let actual = Command::new(proc)
        .stdin(Stdio::from(input))
        .output()
        .map_err(|e| e.to_string())?;

    let duration = (Instant::now() - start).as_millis();
    if expected.as_bytes() == actual.stdout {
        Ok((TestResult::TestSucceeded, duration))
    } else {
        let actual_string = String::from_utf8(actual.stdout).map_err(|e| e.to_string())?;
        Ok((TestResult::TestFailed(expected, actual_string), duration))
    }
}