Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!("{} {} ({:.2?})", 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!("{} {} ({:.2?})", 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!("{} {} ({:.2?})", 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!("{} {} ({:.2?})", self.error.paint("TIMEOUT"), title, time_taken);
self.print_failure(testcase, stdout, stderr);
}
}
Expand Down
7 changes: 4 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,17 +363,18 @@ impl App {
let ostyle = OutputStyle::from_env(show_whitespace);

let mut num_passed = 0;

let mut total_time = std::time::Duration::ZERO;
for (testcase, test_result) in suite_run {
ostyle.print_result(testcase, &test_result);
total_time += test_result.time_taken();

if test_result.is_success() {
num_passed += 1;
} else if !ignore_failures {
break
}
}
println!("{num_passed}/{num_tests} tests passed");
println!("{num_passed}/{num_tests} tests passed ({:.2?})", total_time);

// 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
140 changes: 113 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,39 @@ 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,
},
}
}

pub fn time_taken(&self) -> Duration {
match self {
TestResult::UnableToRun { .. } => Duration::ZERO,
TestResult::Success { time_taken }
| TestResult::WrongOutput { time_taken, .. }
| TestResult::RuntimeError { time_taken, .. }
| TestResult::Timeout { time_taken, .. } => *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 +98,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 +185,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 +203,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