Skip to content

Commit

Permalink
feat: get last mock oracles params (#4789)
Browse files Browse the repository at this point in the history
Resolves #4652

# Description

This adds a new foreign call `get_mock_last_params` that can be used to
configure mocked oracles. It returns the last parameters the mock was
called with, or panics if it wasn't.

Note that this doesn't play very nicely with the `times` feature, since
e.g. if a mock is setup with `times(1)` it is then impossible to ever
retrieve the last params, as the mock will be automatically cleared.
There's a bunch of ways this could be handled, including keeping track
of cleared mocks and llowing these queries over them. I didn't stress
too much about this since we're only now starting to use the mocks, and
I think it might be better to experiment more with them and get more
feedback before worrying about having a nicer API.

I also moved the mock oracle tests outside of `execution_success` into
`noir_test_success`, and added a few simpler cases that I think also
help illustrate how the mocks are supposed to be used (which I
personally found quite useful). I considered adding tests for the
scenarios in which the mocked oracle engine panics (e.g. if no mock
matches the foreign call, or if requesting last params for a mock that
was never called), but run into quite a lot of trouble getting this
working and ultimately gave up. I did at least improve a misleading
error message.

---------

Co-authored-by: Tom French <tom@tomfren.ch>
  • Loading branch information
nventuro and TomAFrench committed Apr 12, 2024
1 parent ce1e662 commit 1d96937
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 35 deletions.
7 changes: 7 additions & 0 deletions noir_stdlib/src/test.nr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ unconstrained fn create_mock_oracle<N>(name: str<N>) -> Field {}
#[oracle(set_mock_params)]
unconstrained fn set_mock_params_oracle<P>(id: Field, params: P) {}

#[oracle(get_mock_last_params)]
unconstrained fn get_mock_last_params_oracle<P>(id: Field) -> P {}

#[oracle(set_mock_returns)]
unconstrained fn set_mock_returns_oracle<R>(id: Field, returns: R) {}

Expand All @@ -27,6 +30,10 @@ impl OracleMock {
self
}

unconstrained pub fn get_last_params<P>(self) -> P {
get_mock_last_params_oracle(self.id)
}

unconstrained pub fn returns<R>(self, returns: R) -> Self {
set_mock_returns_oracle(self.id, returns);
self
Expand Down
2 changes: 0 additions & 2 deletions test_programs/execution_success/mock_oracle/Prover.toml

This file was deleted.

27 changes: 0 additions & 27 deletions test_programs/execution_success/mock_oracle/src/main.nr

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
name = "mock_oracle"
type = "bin"
authors = [""]
compiler_version = ">=0.23.0"

[dependencies]
[dependencies]
Empty file.
130 changes: 130 additions & 0 deletions test_programs/noir_test_success/mock_oracle/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use dep::std::test::OracleMock;

struct Point {
x: Field,
y: Field,
}

impl Eq for Point {
fn eq(self, other: Point) -> bool {
(self.x == other.x) & (self.y == other.y)
}
}

#[oracle(void_field)]
unconstrained fn void_field_oracle() -> Field {}

unconstrained fn void_field() -> Field {
void_field_oracle()
}

#[oracle(field_field)]
unconstrained fn field_field_oracle(_x: Field) -> Field {}

unconstrained fn field_field(x: Field) -> Field {
field_field_oracle(x)
}

#[oracle(struct_field)]
unconstrained fn struct_field_oracle(_point: Point, _array: [Field; 4]) -> Field {}

unconstrained fn struct_field(point: Point, array: [Field; 4]) -> Field {
struct_field_oracle(point, array)
}

#[test(should_fail)]
fn test_mock_no_returns() {
OracleMock::mock("void_field");
void_field(); // Some return value must be set
}

#[test]
fn test_mock() {
OracleMock::mock("void_field").returns(10);
assert_eq(void_field(), 10);
}

#[test]
fn test_multiple_mock() {
let first_mock = OracleMock::mock("void_field").returns(10);
OracleMock::mock("void_field").returns(42);

// The mocks are searched for in creation order, so the first one prevents the second from being called.
assert_eq(void_field(), 10);

first_mock.clear();
assert_eq(void_field(), 42);
}

#[test]
fn test_multiple_mock_times() {
OracleMock::mock("void_field").returns(10).times(2);
OracleMock::mock("void_field").returns(42);

assert_eq(void_field(), 10);
assert_eq(void_field(), 10);
assert_eq(void_field(), 42);
}

#[test]
fn test_mock_with_params() {
OracleMock::mock("field_field").with_params((5,)).returns(10);
assert_eq(field_field(5), 10);
}

#[test]
fn test_multiple_mock_with_params() {
OracleMock::mock("field_field").with_params((5,)).returns(10);
OracleMock::mock("field_field").with_params((7,)).returns(14);

assert_eq(field_field(5), 10);
assert_eq(field_field(7), 14);
}

#[test]
fn test_mock_last_params() {
let mock = OracleMock::mock("field_field").returns(10);
assert_eq(field_field(5), 10);

assert_eq(mock.get_last_params(), 5);
}

#[test]
fn test_mock_last_params_many_calls() {
let mock = OracleMock::mock("field_field").returns(10);
assert_eq(field_field(5), 10);
assert_eq(field_field(7), 10);

assert_eq(mock.get_last_params(), 7);
}

#[test]
fn test_mock_struct_field() {
// Combination of simpler test cases

let array = [1, 2, 3, 4];
let another_array = [4, 3, 2, 1];
let point = Point { x: 14, y: 27 };

OracleMock::mock("struct_field").returns(42).times(2);
let timeless_mock = OracleMock::mock("struct_field").returns(0);

assert_eq(42, struct_field(point, array));
assert_eq(42, struct_field(point, array));
// The times(2) mock is now cleared

assert_eq(0, struct_field(point, array));

let last_params: (Point, [Field; 4]) = timeless_mock.get_last_params();
assert_eq(last_params.0, point);
assert_eq(last_params.1, array);

// We clear the mock with no times() to allow other mocks to be callable
timeless_mock.clear();

OracleMock::mock("struct_field").with_params((point, array)).returns(10);
OracleMock::mock("struct_field").with_params((point, another_array)).returns(20);
assert_eq(10, struct_field(point, array));
assert_eq(20, struct_field(point, another_array));
}

38 changes: 33 additions & 5 deletions tooling/nargo/src/ops/foreign_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub enum ForeignCall {
AssertMessage,
CreateMock,
SetMockParams,
GetMockLastParams,
SetMockReturns,
SetMockTimes,
ClearMock,
Expand All @@ -93,6 +94,7 @@ impl ForeignCall {
ForeignCall::AssertMessage => "assert_message",
ForeignCall::CreateMock => "create_mock",
ForeignCall::SetMockParams => "set_mock_params",
ForeignCall::GetMockLastParams => "get_mock_last_params",
ForeignCall::SetMockReturns => "set_mock_returns",
ForeignCall::SetMockTimes => "set_mock_times",
ForeignCall::ClearMock => "clear_mock",
Expand All @@ -105,6 +107,7 @@ impl ForeignCall {
"assert_message" => Some(ForeignCall::AssertMessage),
"create_mock" => Some(ForeignCall::CreateMock),
"set_mock_params" => Some(ForeignCall::SetMockParams),
"get_mock_last_params" => Some(ForeignCall::GetMockLastParams),
"set_mock_returns" => Some(ForeignCall::SetMockReturns),
"set_mock_times" => Some(ForeignCall::SetMockTimes),
"clear_mock" => Some(ForeignCall::ClearMock),
Expand All @@ -122,6 +125,8 @@ struct MockedCall {
name: String,
/// Optionally match the parameters
params: Option<Vec<ForeignCallParam>>,
/// The parameters with which the mock was last called
last_called_params: Option<Vec<ForeignCallParam>>,
/// The result to return when this mock is called
result: ForeignCallResult,
/// How many times should this mock be called before it is removed
Expand All @@ -134,6 +139,7 @@ impl MockedCall {
id,
name,
params: None,
last_called_params: None,
result: ForeignCallResult { values: vec![] },
times_left: None,
}
Expand Down Expand Up @@ -185,7 +191,11 @@ impl DefaultForeignCallExecutor {
Ok((id, params))
}

fn find_mock_by_id(&mut self, id: usize) -> Option<&mut MockedCall> {
fn find_mock_by_id(&self, id: usize) -> Option<&MockedCall> {
self.mocked_responses.iter().find(|response| response.id == id)
}

fn find_mock_by_id_mut(&mut self, id: usize) -> Option<&mut MockedCall> {
self.mocked_responses.iter_mut().find(|response| response.id == id)
}

Expand Down Expand Up @@ -250,15 +260,27 @@ impl ForeignCallExecutor for DefaultForeignCallExecutor {
}
Some(ForeignCall::SetMockParams) => {
let (id, params) = Self::extract_mock_id(&foreign_call.inputs)?;
self.find_mock_by_id(id)
self.find_mock_by_id_mut(id)
.unwrap_or_else(|| panic!("Unknown mock id {}", id))
.params = Some(params.to_vec());

Ok(ForeignCallResult::default().into())
}
Some(ForeignCall::GetMockLastParams) => {
let (id, _) = Self::extract_mock_id(&foreign_call.inputs)?;
let mock =
self.find_mock_by_id(id).unwrap_or_else(|| panic!("Unknown mock id {}", id));

let last_called_params = mock
.last_called_params
.clone()
.unwrap_or_else(|| panic!("Mock {} was never called", mock.name));

Ok(last_called_params.into())
}
Some(ForeignCall::SetMockReturns) => {
let (id, params) = Self::extract_mock_id(&foreign_call.inputs)?;
self.find_mock_by_id(id)
self.find_mock_by_id_mut(id)
.unwrap_or_else(|| panic!("Unknown mock id {}", id))
.result = ForeignCallResult { values: params.to_vec() };

Expand All @@ -269,7 +291,7 @@ impl ForeignCallExecutor for DefaultForeignCallExecutor {
let times =
params[0].unwrap_field().try_to_u64().expect("Invalid bit size of times");

self.find_mock_by_id(id)
self.find_mock_by_id_mut(id)
.unwrap_or_else(|| panic!("Unknown mock id {}", id))
.times_left = Some(times);

Expand All @@ -292,6 +314,9 @@ impl ForeignCallExecutor for DefaultForeignCallExecutor {
.mocked_responses
.get_mut(response_position)
.expect("Invalid position of mocked response");

mock.last_called_params = Some(foreign_call.inputs.clone());

let result = mock.result.values.clone();

if let Some(times_left) = &mut mock.times_left {
Expand All @@ -316,7 +341,10 @@ impl ForeignCallExecutor for DefaultForeignCallExecutor {

Ok(parsed_response.into())
}
(None, None) => panic!("Unknown foreign call {}", foreign_call_name),
(None, None) => panic!(
"No mock for foreign call {}({:?})",
foreign_call_name, &foreign_call.inputs
),
}
}
}
Expand Down

0 comments on commit 1d96937

Please sign in to comment.