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: Handle should_fail_with case #2541

Merged
merged 54 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
387b541
feat: assertion messages for acir and brillig
sirasistant Aug 21, 2023
a0f1b8c
rewrite assert msg handling
sirasistant Aug 23, 2023
268d42c
refactor: improve error handling
sirasistant Aug 30, 2023
f4af108
style: error message
sirasistant Aug 30, 2023
622b82d
Merge branch 'master' into arv/assert_msgs
sirasistant Aug 31, 2023
1c4af96
refactor: improving parser
sirasistant Aug 31, 2023
2520f39
fix: assert_eq and comptime messages
sirasistant Aug 31, 2023
9ef7340
test: updated parser tests
sirasistant Aug 31, 2023
bc87640
acvm 0.24
kevaundray Aug 31, 2023
e4fcc39
add artifacts
kevaundray Aug 31, 2023
fb538ae
style: remove old todo
sirasistant Aug 31, 2023
0432010
test: reorder parser test for clarity
sirasistant Aug 31, 2023
8a8f347
Merge branch 'master' into arv/assert_msgs
sirasistant Aug 31, 2023
af96ce4
test: added assert msgs in e2e
sirasistant Aug 31, 2023
71b9664
Merge remote-tracking branch 'origin/master' into kw/acvm-0-24
kevaundray Aug 31, 2023
cc7a37f
missed file
kevaundray Aug 31, 2023
0b98ae3
change to branch
kevaundray Aug 31, 2023
ff135e8
update bytecode
kevaundray Aug 31, 2023
ac03515
change trait definitions
kevaundray Aug 31, 2023
b20fcac
rebuild script
kevaundray Aug 31, 2023
6b82986
fix trait call and remove dirs
kevaundray Aug 31, 2023
03dcd55
Merge remote-tracking branch 'origin/master' into kw/acvm-0-24
kevaundray Sep 1, 2023
48e9d1d
Merge remote-tracking branch 'origin/master' into kw/acvm-0-24
kevaundray Sep 1, 2023
83c487d
use nightly
kevaundray Sep 1, 2023
6a7ff1a
commit to rebuild
kevaundray Sep 1, 2023
2d39ed7
update 1_mul.bytecode
kevaundray Sep 1, 2023
145e735
Merge branch 'kw/acvm-0-24' into arv/assert_msgs
sirasistant Sep 1, 2023
7166321
Merge branch 'kw/acvm-0-24' into arv/assert_msgs
sirasistant Sep 1, 2023
b813e05
update to 2788
kevaundray Sep 1, 2023
4ecbd3c
refactor: use closure for resolving asserts
sirasistant Sep 1, 2023
2f82a75
Merge branch 'kw/acvm-0-24' into arv/assert_msgs
sirasistant Sep 1, 2023
223d09c
Update Cargo.toml
kevaundray Sep 1, 2023
d5fae75
Merge remote-tracking branch 'origin/master' into kw/acvm-0-24
kevaundray Sep 1, 2023
fc1172a
update cargo.lock
kevaundray Sep 1, 2023
d115fde
patch acvm traits which have disappeared with acvm-backend-barretenberg
kevaundray Sep 1, 2023
4a5e31b
use bb 0.5.0
kevaundray Sep 1, 2023
1bb8e50
remove backend specific methods
kevaundray Sep 1, 2023
3c44114
Merge remote-tracking branch 'origin/master' into kw/acvm-0-24
kevaundray Sep 3, 2023
179da4f
Merge remote-tracking branch 'origin/kw/acvm-0-24' into arv/assert_msgs
kevaundray Sep 3, 2023
8ec3c24
add should_fail_with into lexer
kevaundray Sep 3, 2023
bc4c28c
add ShouldFailWith to TestFunction and expose `failure_reason`
kevaundray Sep 3, 2023
54ffa3b
handle should_fail_with case
kevaundray Sep 3, 2023
b4b4451
Merge remote-tracking branch 'origin/master' into kw/should-fail-with
kevaundray Sep 7, 2023
6c9a371
use one enum
kevaundray Sep 7, 2023
9885ef6
split match branch into two separate functions
kevaundray Sep 7, 2023
f4a985f
cleanup code a bit more
kevaundray Sep 7, 2023
9eb7ef4
Merge remote-tracking branch 'origin/master' into kw/should-fail-with
kevaundray Sep 7, 2023
4beb507
change match to equal
kevaundray Sep 7, 2023
8d6b563
add convenient method to extract the user defined error message
kevaundray Sep 7, 2023
8358adf
refactor test
kevaundray Sep 7, 2023
40161a7
remove extra allocation
kevaundray Sep 7, 2023
bb4b199
Update crates/nargo_cli/src/cli/execute_cmd.rs
kevaundray Sep 7, 2023
7825726
refactor test
kevaundray Sep 7, 2023
d5b4c24
Update crates/nargo/src/ops/test.rs
kevaundray Sep 7, 2023
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
26 changes: 26 additions & 0 deletions crates/nargo/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ pub enum NargoError {
ForeignCallError(#[from] ForeignCallError),
}

impl NargoError {
/// Extracts the user defined failure message from the ExecutionError
/// If one exists.
///
/// We want to extract the user defined error so that we can compare it
/// in tests to expected failure messages
pub fn user_defined_failure_message(&self) -> Option<&str> {
let execution_error = match self {
NargoError::ExecutionError(error) => error,
_ => return None,
};

match execution_error {
ExecutionError::AssertionFailed(message, _) => Some(message),
ExecutionError::SolvingError(error) => match error {
OpcodeResolutionError::IndexOutOfBounds { .. }
| OpcodeResolutionError::UnsupportedBlackBoxFunc(_)
| OpcodeResolutionError::OpcodeNotSolvable(_)
| OpcodeResolutionError::UnsatisfiedConstrain { .. } => None,
OpcodeResolutionError::BrilligFunctionFailed { message, .. } => Some(message),
OpcodeResolutionError::BlackBoxFunctionFailed(_, reason) => Some(reason),
},
}
}
}

#[derive(Debug, Error)]
pub enum ExecutionError {
#[error("Failed assertion: '{}'", .0)]
Expand Down
122 changes: 89 additions & 33 deletions crates/nargo/src/ops/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use noirc_driver::{compile_no_check, CompileOptions};
use noirc_errors::FileDiagnostic;
use noirc_frontend::hir::{def_map::TestFunction, Context};

use crate::NargoError;

use super::execute_circuit;

pub enum TestStatus {
Expand All @@ -25,44 +27,98 @@ pub fn run_test<B: BlackBoxFunctionSolver>(
// otherwise constraints involving these expressions will not error.
let circuit_execution =
execute_circuit(blackbox_solver, program.circuit, WitnessMap::new(), show_output);
test_status_program_compile_pass(test_function, circuit_execution)
vezenovm marked this conversation as resolved.
Show resolved Hide resolved
}
Err(diag) => test_status_program_compile_fail(diag, test_function),
}
}

/// Test function failed to compile
///
/// Note: This could be because the compiler was able to deduce
/// that a constraint was never satisfiable.
/// An example of this is the program `assert(false)`
/// In that case, we check if the test function should fail, and if so, we return `TestStatus::Pass`.
fn test_status_program_compile_fail(
diag: FileDiagnostic,
test_function: TestFunction,
) -> TestStatus {
// The test has failed compilation, but it should never fail. Report error.
if !test_function.should_fail() {
return TestStatus::CompileError(diag);
}

// The test has failed compilation, check if it is because the program is never satisfiable.
// If it is never satisfiable, then this is the expected behavior.
let program_is_never_satisfiable = diag.diagnostic.message.contains("Failed constraint");
if !program_is_never_satisfiable {
// The test has failed compilation, but its a compilation error. Report error
return TestStatus::CompileError(diag);
}

check_expected_failure_message(test_function, &diag.diagnostic.message)
}

/// The test function compiled successfully.
///
/// We now check whether execution passed/failed and whether it should have
/// passed/failed to determine the test status.
fn test_status_program_compile_pass(
vezenovm marked this conversation as resolved.
Show resolved Hide resolved
test_function: TestFunction,
circuit_execution: Result<WitnessMap, NargoError>,
) -> TestStatus {
let circuit_execution_err = match circuit_execution {
// Circuit execution was successful; ie no errors or unsatisfied constraints
// were encountered.
Ok(_) => {
if test_function.should_fail() {
match circuit_execution {
Ok(_) => TestStatus::Fail {
// TODO: Improve color variations on this message
message: "error: Test passed when it should have failed".to_string(),
},
Err(_) => TestStatus::Pass,
}
} else {
match circuit_execution {
Ok(_) => TestStatus::Pass,
Err(error) => TestStatus::Fail { message: error.to_string() },
}
return TestStatus::Fail {
message: "error: Test passed when it should have failed".to_string(),
};
}
return TestStatus::Pass;
}
// Test function failed to compile
//
// Note: This could be because the compiler was able to deduce
// that a constraint was never satisfiable.
// An example of this is the program `assert(false)`
// In that case, we check if the test function should fail, and if so, we return `TestStatus::Pass`.
Err(diag) => {
// The test has failed compilation, but it should never fail. Report error.
if !test_function.should_fail() {
return TestStatus::CompileError(diag);
}
Err(err) => err,
};

// The test has failed compilation, check if it is because the program is never satisfiable.
// If it is never satisfiable, then this is the expected behavior.
let program_is_never_satisfiable =
diag.diagnostic.message.contains("Failed constraint");
if program_is_never_satisfiable {
return TestStatus::Pass;
}
// If we reach here, then the circuit execution failed.
//
// Check if the function should have passed
let test_should_have_passed = !test_function.should_fail();
if test_should_have_passed {
return TestStatus::Fail { message: circuit_execution_err.to_string() };
}

// The test has failed compilation, but its a compilation error. Report error
TestStatus::CompileError(diag)
}
check_expected_failure_message(
test_function,
circuit_execution_err.user_defined_failure_message().unwrap_or_default(),
)
}

fn check_expected_failure_message(test_function: TestFunction, got_error: &str) -> TestStatus {
// Extract the expected failure message, if there was one
//
// #[test(should_fail)] will not produce any message
// #[test(should_fail_with = "reason")] will produce a message
//
let expected_failure_message = match test_function.failure_reason() {
Some(reason) => reason,
None => return TestStatus::Pass,
};

let failure_message_in_assert_method = got_error;
let expected_failure_message_matches =
failure_message_in_assert_method == expected_failure_message;
kevaundray marked this conversation as resolved.
Show resolved Hide resolved
if expected_failure_message_matches {
return TestStatus::Pass;
}

// The expected failure message does not match the actual failure message
return TestStatus::Fail {
message: format!(
"\nerror: Test failed with the wrong message. \nExpected: {} \nGot: {}",
test_function.failure_reason().unwrap_or_default(),
got_error.trim_matches('\'')
),
};
}
12 changes: 11 additions & 1 deletion crates/noirc_frontend/src/hir/def_map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,20 @@ impl TestFunction {

/// Returns true if the test function has been specified to fail
/// This is done by annotating the function with `#[test(should_fail)]`
/// or `#[test(should_fail_with = "reason")]`
pub fn should_fail(&self) -> bool {
match self.scope {
TestScope::ShouldFail => true,
TestScope::ShouldFailWith { .. } => true,
TestScope::None => false,
}
}

/// Returns the reason for the test function to fail if specified
/// by the user.
pub fn failure_reason(&self) -> Option<&str> {
match &self.scope {
TestScope::None => None,
TestScope::ShouldFailWith { reason } => reason.as_deref(),
}
}
}
19 changes: 18 additions & 1 deletion crates/noirc_frontend/src/lexer/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,24 @@ mod tests {
let mut lexer = Lexer::new(input);

let token = lexer.next().unwrap().unwrap();
assert_eq!(token.token(), &Token::Attribute(Attribute::Test(TestScope::ShouldFail)));
assert_eq!(
token.token(),
&Token::Attribute(Attribute::Test(TestScope::ShouldFailWith { reason: None }))
);
}

#[test]
fn test_attribute_with_valid_scope_should_fail_with() {
let input = r#"#[test(should_fail_with = "hello")]"#;
let mut lexer = Lexer::new(input);

let token = lexer.next().unwrap().unwrap();
assert_eq!(
token.token(),
&Token::Attribute(Attribute::Test(TestScope::ShouldFailWith {
reason: Some("hello".to_owned())
}))
);
}

#[test]
Expand Down
37 changes: 26 additions & 11 deletions crates/noirc_frontend/src/lexer/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,30 +319,42 @@ impl IntType {
/// TestScope is used to specify additional annotations for test functions
#[derive(PartialEq, Eq, Hash, Debug, Clone, PartialOrd, Ord)]
pub enum TestScope {
/// If a test has a scope of ShouldFail, then it is expected to fail
ShouldFail,
/// If a test has a scope of ShouldFailWith, then it can only pass
/// if it fails with the specified reason. If the reason is None, then
/// the test must unconditionally fail
ShouldFailWith { reason: Option<String> },
/// No scope is applied and so the test must pass
None,
}

impl TestScope {
fn lookup_str(string: &str) -> Option<TestScope> {
kevaundray marked this conversation as resolved.
Show resolved Hide resolved
match string {
"should_fail" => Some(TestScope::ShouldFail),
match string.trim() {
"should_fail" => Some(TestScope::ShouldFailWith { reason: None }),
s if s.starts_with("should_fail_with") => {
let parts: Vec<&str> = s.splitn(2, '=').collect();
if parts.len() == 2 {
let reason = parts[1].trim();
let reason = reason.trim_matches('"');
Some(TestScope::ShouldFailWith { reason: Some(reason.to_string()) })
} else {
None
}
}
_ => None,
}
}
fn as_str(&self) -> &'static str {
match self {
TestScope::ShouldFail => "should_fail",
TestScope::None => "",
}
}
}

impl fmt::Display for TestScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({})", self.as_str())
match self {
TestScope::None => write!(f, ""),
TestScope::ShouldFailWith { reason } => match reason {
Some(failure_reason) => write!(f, "(should_fail_with = ({}))", failure_reason),
None => write!(f, "should_fail"),
},
}
}
}

Expand Down Expand Up @@ -391,6 +403,9 @@ impl Attribute {
|| ch == '_'
|| ch == '('
|| ch == ')'
|| ch == '='
|| ch == '"'
|| ch == ' '
})
.then_some(());

Expand Down
Loading