Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add possibility to interrupt running test #521

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
21 changes: 21 additions & 0 deletions proptest/src/test_runner/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use crate::test_runner::Reason;
/// `Error::display()` into the `Fail` case.
#[derive(Debug, Clone)]
pub enum TestCaseError {
/// The test runner was interrupted.
Interrupt(Reason),
/// The input was not valid for the test case. This does not count as a
/// test failure (nor a success); rather, it simply signals to generate
/// a new input and try again.
Expand Down Expand Up @@ -61,6 +63,16 @@ pub type TestCaseResult = Result<(), TestCaseError>;
pub(crate) type TestCaseResultV2 = Result<TestCaseOk, TestCaseError>;

impl TestCaseError {
/// Interrupts this test case runner. This does not count as a test failure
/// (nor a success); rather, it simply signals to stop executing new runs,
/// regardless number of configured successful test cases.
///
/// The string gives the location and context of the interruption, and
/// should be suitable for formatting like `Foo did X at {whence}`.
pub fn interrupt(reason: impl Into<Reason>) -> Self {
TestCaseError::Interrupt(reason.into())
}

/// Rejects the generated test input as invalid for this test case. This
/// does not count as a test failure (nor a success); rather, it simply
/// signals to generate a new input and try again.
Expand All @@ -83,6 +95,9 @@ impl TestCaseError {
impl fmt::Display for TestCaseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TestCaseError::Interrupt(ref why) => {
write!(f, "Case interrupted: {}", why)
}
TestCaseError::Reject(ref whence) => {
write!(f, "Input rejected at {}", whence)
}
Expand All @@ -101,6 +116,8 @@ impl<E: ::std::error::Error> From<E> for TestCaseError {
/// A failure state from running test cases for a single test.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TestError<T> {
/// The test was interrupted for the given reason.
Interrupt(Reason),
/// The test was aborted for the given reason, for example, due to too many
/// inputs having been rejected.
Abort(Reason),
Expand All @@ -113,6 +130,9 @@ pub enum TestError<T> {
impl<T: fmt::Debug> fmt::Display for TestError<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TestError::Interrupt(ref why) => {
write!(f, "Test interrupted: {}", why)
}
TestError::Abort(ref why) => write!(f, "Test aborted: {}", why),
TestError::Fail(ref why, ref what) => {
writeln!(f, "Test failed: {}.", why)?;
Expand All @@ -127,6 +147,7 @@ impl<T: fmt::Debug> fmt::Display for TestError<T> {
impl<T: fmt::Debug> ::std::error::Error for TestError<T> {
fn description(&self) -> &str {
match *self {
TestError::Interrupt(..) => "Interrupted",
TestError::Abort(..) => "Abort",
TestError::Fail(..) => "Fail",
}
Expand Down
2 changes: 1 addition & 1 deletion proptest/src/test_runner/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ fn step_to_char(step: &TestCaseResult) -> char {
match *step {
Ok(_) => '+',
Err(TestCaseError::Reject(_)) => '!',
Err(TestCaseError::Fail(_)) => '-',
Err(TestCaseError::Fail(_)) | Err(TestCaseError::Interrupt(_)) => '-',
}
}

Expand Down
68 changes: 68 additions & 0 deletions proptest/src/test_runner/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ where

match result {
Ok(()) => verbose_message!(runner, TRACE, "Test case passed"),
Err(TestCaseError::Interrupt(ref reason)) => {
verbose_message!(
runner,
INFO_LOG,
"Test case interrupted: {}",
reason
)
}
Err(TestCaseError::Reject(ref reason)) => {
verbose_message!(runner, INFO_LOG, "Test case rejected: {}", reason)
}
Expand Down Expand Up @@ -620,6 +628,12 @@ impl TestRunner {
&mut fork_output,
false,
);

if let Err(TestError::Interrupt(_)) = result {
// exit runs loop if test was interrupted
break;
}

if let Err(TestError::Fail(_, ref value)) = result {
if let Some(ref mut failure_persistence) =
self.config.failure_persistence
Expand Down Expand Up @@ -746,6 +760,9 @@ impl TestRunner {
.unwrap_or(why);
Err(TestError::Fail(why, case.current()))
}
Err(TestCaseError::Interrupt(why)) => {
Err(TestError::Interrupt(why))
}
Err(TestCaseError::Reject(whence)) => {
self.reject_global(whence)?;
Ok(TestCaseOk::Reject)
Expand Down Expand Up @@ -883,6 +900,7 @@ impl TestRunner {
break;
}
}
Err(TestCaseError::Interrupt(_)) => {}
}
}
}
Expand Down Expand Up @@ -1094,6 +1112,56 @@ mod test {
assert_eq!(Ok(()), result);
}

#[test]
fn test_interrupt_at_run() {
let mut runner = TestRunner::new(Config {
failure_persistence: None,
cases: 20,
..Config::default()
});

let run_count = RefCell::new(0);
let result = runner.run(&(1u32..), |_v| {
*run_count.borrow_mut() += 1;
if *run_count.borrow() == 10 {
return Err(TestCaseError::Interrupt(Reason::from(
"test interrupted",
)));
}
Ok(())
});
// only 10 runs performed
assert_eq!(run_count.into_inner(), 10);
// interrupt does not count as fail
assert_eq!(Ok(()), result);
}

#[test]
fn test_interrupt_with_failed_case() {
let mut runner = TestRunner::new(Config {
failure_persistence: None,
cases: 20,
..Config::default()
});

let run_count = RefCell::new(0);
let _ = runner.run(&(0u32..10u32), |v| {
*run_count.borrow_mut() += 1;
if v < 5 {
if *run_count.borrow() == 10 {
return Err(TestCaseError::Interrupt(Reason::from(
"test interrupted",
)));
}
Ok(())
} else {
Err(TestCaseError::fail("not less than 5"))
}
});
// no more than 10 runs
assert!(run_count.into_inner() <= 10);
}

#[test]
fn test_fail_via_result() {
let mut runner = TestRunner::new(Config {
Expand Down
Loading