Skip to content

Commit

Permalink
display test case runtimes
Browse files Browse the repository at this point in the history
  • Loading branch information
Andriamanitra committed Jan 20, 2025
1 parent 8068757 commit d78f4fd
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 40 deletions.
28 changes: 20 additions & 8 deletions src/internal/outputstyle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,27 +221,39 @@ impl OutputStyle {
pub fn print_result(&self, testcase: &Testcase, test_result: &TestResult) {
let title = self.styled_testcase_title(testcase);
match test_result {
TestResult::Success => {
println!("{} {}", self.success.paint("PASS"), title);
TestResult::Success { time_taken } => {
println!("{} {} ({:.0?})", self.success.paint("PASS"), title, time_taken);
}

TestResult::UnableToRun { error_msg } => {
println!("{} {}", self.failure.paint("ERROR"), title);
println!(" {}", self.stderr.paint(error_msg));
}

TestResult::WrongOutput { stdout, stderr } => {
println!("{} {}", self.failure.paint("FAIL"), title);
TestResult::WrongOutput {
stdout,
stderr,
time_taken,
} => {
println!("{} {} ({:.0?})", self.failure.paint("FAIL"), title, time_taken);
self.print_failure(testcase, stdout, stderr);
}

TestResult::RuntimeError { stdout, stderr } => {
println!("{} {}", self.error.paint("ERROR"), title);
TestResult::RuntimeError {
stdout,
stderr,
time_taken,
} => {
println!("{} {} ({:.0?})", self.error.paint("ERROR"), title, time_taken);
self.print_failure(testcase, stdout, stderr);
}

TestResult::Timeout { stdout, stderr } => {
println!("{} {}", self.error.paint("TIMEOUT"), title);
TestResult::Timeout {
stdout,
stderr,
time_taken,
} => {
println!("{} {} ({:.0?})", self.error.paint("TIMEOUT"), title, time_taken);
self.print_failure(testcase, stdout, stderr);
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,9 @@ impl App {
.expect("clap should ensure `run` can't be executed without a --command");

let timeout = match *args.get_one::<f64>("timeout").unwrap_or(&5.0) {
0.0 => std::time::Duration::MAX,
secs if secs.is_nan() => return Err(anyhow!("Timeout can't be NaN")),
secs if secs < 0.0 => return Err(anyhow!("Timeout can't be negative (use 0 for no timeout)")),
secs if secs == 0.0 => std::time::Duration::MAX,
secs => std::time::Duration::from_micros((secs * 1e6) as u64),
};

Expand All @@ -363,7 +363,7 @@ impl App {
let ostyle = OutputStyle::from_env(show_whitespace);

let mut num_passed = 0;

let start_time = std::time::Instant::now();
for (testcase, test_result) in suite_run {
ostyle.print_result(testcase, &test_result);

Expand All @@ -373,7 +373,7 @@ impl App {
break
}
}
println!("{num_passed}/{num_tests} tests passed");
println!("{num_passed}/{num_tests} tests passed ({}ms)", start_time.elapsed().as_millis());

// Move on to next clash if --auto-advance is set
if num_passed == num_tests && args.get_flag("auto-advance") {
Expand Down
6 changes: 4 additions & 2 deletions src/solution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod test_result;

use std::io::Write;
use std::process::Command;
use std::time::Duration;
use std::time::{Duration, Instant};

use test_result::CommandExit;
pub use test_result::TestResult;
Expand Down Expand Up @@ -48,6 +48,7 @@ pub fn lazy_run<'a>(

/// Run a command against a single testcase.
pub fn run_testcase(testcase: &Testcase, run_command: &mut Command, timeout: &Duration) -> TestResult {
let start_time = Instant::now();
let mut run = match run_command
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
Expand Down Expand Up @@ -77,6 +78,7 @@ pub fn run_testcase(testcase: &Testcase, run_command: &mut Command, timeout: &Du
run.kill().expect("Process should have been killed");
}

let time_taken = start_time.elapsed();
let output = run.wait_with_output().expect("Process should allow waiting for its execution");

let exit_status = if timed_out {
Expand All @@ -86,7 +88,7 @@ pub fn run_testcase(testcase: &Testcase, run_command: &mut Command, timeout: &Du
} else {
CommandExit::Error
};
TestResult::from_output(&testcase.test_out, output.stdout, output.stderr, exit_status)
TestResult::from_output(&testcase.test_out, output.stdout, output.stderr, exit_status, time_taken)
}

#[cfg(test)]
Expand Down
130 changes: 103 additions & 27 deletions src/solution/test_result.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

pub enum CommandExit {
Ok,
Error,
Expand All @@ -12,18 +14,30 @@ pub enum TestResult {
/// Solution command produced the expected output. A test run is considered
/// a success even if it runs into a runtime error or times out if its
/// output was correct (just like it works on CodinGame).
Success,
Success { time_taken: Duration },
/// Solution command failed to run. This may happen for example if the
/// executable does not exist or if the current user does not have
/// permission to execute it.
UnableToRun { error_msg: String },
/// Solution command exited normally but did not produce the expected
/// output.
WrongOutput { stdout: String, stderr: String },
WrongOutput {
stdout: String,
stderr: String,
time_taken: Duration,
},
/// Solution command encountered a runtime error (exited non-zero).
RuntimeError { stdout: String, stderr: String },
RuntimeError {
stdout: String,
stderr: String,
time_taken: Duration,
},
/// Solution command timed out.
Timeout { stdout: String, stderr: String },
Timeout {
stdout: String,
stderr: String,
time_taken: Duration,
},
}

impl TestResult {
Expand All @@ -32,6 +46,7 @@ impl TestResult {
stdout: Vec<u8>,
stderr: Vec<u8>,
exit_status: CommandExit,
time_taken: Duration,
) -> Self {
let stdout = String::from_utf8(stdout)
.unwrap_or_default()
Expand All @@ -41,17 +56,29 @@ impl TestResult {
let stderr = String::from_utf8(stderr).unwrap_or_default();

match exit_status {
_ if stdout == expected.trim_end() => TestResult::Success,
CommandExit::Timeout => TestResult::Timeout { stdout, stderr },
CommandExit::Ok => TestResult::WrongOutput { stdout, stderr },
CommandExit::Error => TestResult::RuntimeError { stdout, stderr },
_ if stdout == expected.trim_end() => TestResult::Success { time_taken },
CommandExit::Timeout => TestResult::Timeout {
stdout,
stderr,
time_taken,
},
CommandExit::Ok => TestResult::WrongOutput {
stdout,
stderr,
time_taken,
},
CommandExit::Error => TestResult::RuntimeError {
stdout,
stderr,
time_taken,
},
}
}

/// Returns true if the testcase passed. A testcase passes if the output
/// of the solution command matches the expected output.
pub fn is_success(&self) -> bool {
matches!(self, TestResult::Success)
matches!(self, TestResult::Success { .. })
}
}

Expand All @@ -61,47 +88,84 @@ mod tests {

#[test]
fn test_testresult_success() {
let result = TestResult::from_output("123", "123".into(), vec![], CommandExit::Ok);
assert!(matches!(result, TestResult::Success));
let result =
TestResult::from_output("123", "123".into(), vec![], CommandExit::Ok, Duration::from_millis(100));
assert!(matches!(result, TestResult::Success { .. }));
}

#[test]
fn test_testresult_success_with_trailing_whitespace() {
let result = TestResult::from_output("abc\n", "abc".into(), vec![], CommandExit::Ok);
assert!(matches!(result, TestResult::Success));
let result = TestResult::from_output("abc", "abc\r\n".into(), vec![], CommandExit::Ok);
assert!(matches!(result, TestResult::Success));
let result = TestResult::from_output(
"abc\n",
"abc".into(),
vec![],
CommandExit::Ok,
Duration::from_millis(100),
);
assert!(matches!(result, TestResult::Success { .. }));
let result = TestResult::from_output(
"abc",
"abc\r\n".into(),
vec![],
CommandExit::Ok,
Duration::from_millis(100),
);
assert!(matches!(result, TestResult::Success { .. }));
}

#[test]
fn test_testresult_success_normalized_line_endings() {
let result = TestResult::from_output("a\nb\nc", "a\r\nb\r\nc".into(), vec![], CommandExit::Ok);
assert!(matches!(result, TestResult::Success));
let result = TestResult::from_output(
"a\nb\nc",
"a\r\nb\r\nc".into(),
vec![],
CommandExit::Ok,
Duration::from_millis(100),
);
assert!(matches!(result, TestResult::Success { .. }));
}

#[test]
fn test_testresult_success_on_timeout() {
let result = TestResult::from_output("123", "123".into(), vec![], CommandExit::Timeout);
let result = TestResult::from_output(
"123",
"123".into(),
vec![],
CommandExit::Timeout,
Duration::from_millis(100),
);
assert!(
matches!(result, TestResult::Success),
matches!(result, TestResult::Success { .. }),
"TestResult should be `Success` when stdout is correct even if execution timed out"
)
}

#[test]
fn test_testresult_success_on_runtime_error() {
let result = TestResult::from_output("123", "123".into(), vec![], CommandExit::Error);
let result = TestResult::from_output(
"123",
"123".into(),
vec![],
CommandExit::Error,
Duration::from_millis(100),
);
assert!(
matches!(result, TestResult::Success),
matches!(result, TestResult::Success { .. }),
"TestResult should be `Success` when stdout is correct even if a runtime error occurred"
)
}

#[test]
fn test_testresult_wrong_output() {
let result = TestResult::from_output("x\ny\nz", "yyy".into(), "zzz".into(), CommandExit::Ok);
let result = TestResult::from_output(
"x\ny\nz",
"yyy".into(),
"zzz".into(),
CommandExit::Ok,
Duration::from_millis(100),
);
match result {
TestResult::WrongOutput { stdout, stderr } => {
TestResult::WrongOutput { stdout, stderr, .. } => {
assert_eq!(stdout, "yyy");
assert_eq!(stderr, "zzz");
}
Expand All @@ -111,9 +175,15 @@ mod tests {

#[test]
fn test_testresult_timed_out() {
let result = TestResult::from_output("xxx", "yyy".into(), "zzz".into(), CommandExit::Timeout);
let result = TestResult::from_output(
"xxx",
"yyy".into(),
"zzz".into(),
CommandExit::Timeout,
Duration::from_millis(100),
);
match result {
TestResult::Timeout { stdout, stderr } => {
TestResult::Timeout { stdout, stderr, .. } => {
assert_eq!(stdout, "yyy");
assert_eq!(stderr, "zzz");
}
Expand All @@ -123,9 +193,15 @@ mod tests {

#[test]
fn test_testresult_runtime_error() {
let result = TestResult::from_output("xxx", "yyy".into(), "zzz".into(), CommandExit::Error);
let result = TestResult::from_output(
"xxx",
"yyy".into(),
"zzz".into(),
CommandExit::Error,
Duration::from_millis(100),
);
match result {
TestResult::RuntimeError { stdout, stderr } => {
TestResult::RuntimeError { stdout, stderr, .. } => {
assert_eq!(stdout, "yyy");
assert_eq!(stderr, "zzz");
}
Expand Down

0 comments on commit d78f4fd

Please sign in to comment.