diff --git a/src/internal/outputstyle.rs b/src/internal/outputstyle.rs index 9d1f0ee..62c0ff8 100644 --- a/src/internal/outputstyle.rs +++ b/src/internal/outputstyle.rs @@ -221,8 +221,8 @@ 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 } => { @@ -230,18 +230,30 @@ impl OutputStyle { 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); } } diff --git a/src/main.rs b/src/main.rs index 05fae92..98faef0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -341,9 +341,9 @@ impl App { .expect("clap should ensure `run` can't be executed without a --command"); let timeout = match *args.get_one::("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), }; @@ -363,9 +363,10 @@ 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; @@ -373,7 +374,7 @@ impl App { 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") { diff --git a/src/solution.rs b/src/solution.rs index c823345..d8af5d9 100644 --- a/src/solution.rs +++ b/src/solution.rs @@ -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; @@ -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()) @@ -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 { @@ -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)] diff --git a/src/solution/test_result.rs b/src/solution/test_result.rs index e7b0313..2103802 100644 --- a/src/solution/test_result.rs +++ b/src/solution/test_result.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + pub enum CommandExit { Ok, Error, @@ -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 { @@ -32,6 +46,7 @@ impl TestResult { stdout: Vec, stderr: Vec, exit_status: CommandExit, + time_taken: Duration, ) -> Self { let stdout = String::from_utf8(stdout) .unwrap_or_default() @@ -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 { .. }) } } @@ -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"); } @@ -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"); } @@ -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"); }