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(PocketIC): new call response types #3425

Merged
merged 11 commits into from
Jan 16, 2025
7 changes: 6 additions & 1 deletion packages/pocket-ic/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test", "rust_test_suite")
load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test", "rust_test_suite")

package(default_visibility = ["//visibility:public"])

Expand Down Expand Up @@ -51,6 +51,11 @@ rust_library(
deps = DEPENDENCIES,
)

rust_doc_test(
name = "pocket-ic-doc-test",
crate = ":pocket-ic",
)

rust_test(
name = "pocket-ic-test",
srcs = glob(["src/**/*.rs"]),
Expand Down
3 changes: 3 additions & 0 deletions packages/pocket-ic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The function `PocketIc::await_call_no_ticks` to await the status of an update call (submitted through an ingress message) becoming known without triggering round execution
(round execution must be triggered separarely, e.g., on a "live" instance or by separate PocketIC library calls).

### Changed
- The response types `pocket_ic::WasmResult`, `pocket_ic::UserError`, and `pocket_ic::CallError` are replaced by a single reject response type `pocket_ic::RejectResponse`.



## 6.0.0 - 2024-11-13
Expand Down
53 changes: 20 additions & 33 deletions packages/pocket-ic/HOWTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,23 +242,22 @@ Here is a sketch of a test for a canister making canister HTTP outcalls:
fn test_canister_http() {
let pic = PocketIc::new();

// Create a canister and charge it with 100T cycles.
let can_id = pic.create_canister();
pic.add_cycles(can_id, 100_000_000_000_000);
// Create a canister and charge it with 2T cycles.
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 2_000_000_000_000);

// Install the test canister wasm file on the canister.
let test_wasm = [...];
pic.install_canister(can_id, test_wasm, vec![], None);
let test_wasm = todo!();
pic.install_canister(canister_id, test_wasm, vec![], None);

// Submit an update call to the test canister making a canister http outcall
// and mock a canister http outcall response.
let arg_bytes = Encode!(&()).unwrap();
let call_id = pic
.submit_call(
can_id,
canister_id,
Principal::anonymous(),
"canister_http",
arg_bytes,
encode_one(()).unwrap(),
)
.unwrap();

Expand All @@ -283,22 +282,16 @@ fn test_canister_http() {
};
pic.mock_canister_http_response(mock_canister_http_response);

// Now the test canister will receive the http outcall response
// and reply to the ingress message from the test driver
// relaying the received http outcall response.
let reply = pic.await_call(call_id).unwrap();
match reply {
WasmResult::Reply(data) => {
let http_response: Result<HttpResponse, (RejectionCode, String)> =
decode_one(&data).unwrap();
assert_eq!(http_response.unwrap().body, body);
}
WasmResult::Reject(msg) => panic!("Unexpected reject {}", msg),
};

// There should be no more pending canister http outcalls.
let canister_http_requests = pic.get_canister_http();
assert_eq!(canister_http_requests.len(), 0);

// Now the test canister will receive the http outcall response
// and reply to the ingress message from the test driver.
let reply = pic.await_call(call_id).unwrap();
let http_response: Result<HttpRequestResult, (RejectionCode, String)> =
decode_one(&reply).unwrap();
assert_eq!(http_response.unwrap().body, body);
}
```

Expand Down Expand Up @@ -332,18 +325,12 @@ e.g., 13 for a regular application subnet.
// and reply to the ingress message from the test driver
// relaying the error.
let reply = pic.await_call(call_id).unwrap();
match reply {
WasmResult::Reply(data) => {
let http_response: Result<HttpResponse, (RejectionCode, String)> =
decode_one(&data).unwrap();
let (reject_code, err) = http_response.unwrap_err();
assert_eq!(reject_code, RejectionCode::SysTransient);
assert!(
err.contains("No consensus could be reached. Replicas had different responses.")
);
}
WasmResult::Reject(msg) => panic!("Unexpected reject {}", msg),
};
let http_response: Result<HttpRequestResult, (RejectionCode, String)> =
decode_one(&reply).unwrap();
let (reject_code, err) = http_response.unwrap_err();
assert!(matches!(reject_code, RejectionCode::SysTransient));
let expected = "No consensus could be reached. Replicas had different responses. Details: request_id: 0, timeout: 1620328930000000005, hashes: [98387cc077af9cff2ef439132854e91cb074035bb76e2afb266960d8e3beaf11: 2], [6a2fa8e54fb4bbe62cde29f7531223d9fcf52c21c03500c1060a5f893ed32d2e: 2], [3e9ec98abf56ef680bebb14309858ede38f6fde771cd4c04cda8f066dc2810db: 2], [2c14e77f18cd990676ae6ce0d7eb89c0af9e1a66e17294b5f0efa68422bba4cb: 2], [2843e4133f673571ff919808d3ca542cc54aaf288c702944e291f0e4fafffc69: 2], [1c4ad84926c36f1fbc634a0dc0535709706f7c48f0c6ebd814fe514022b90671: 2], [7bf80e2f02011ab0a7836b526546e75203b94e856d767c9df4cb0c19baf34059: 1]";
assert_eq!(err, expected);
```

In the live mode (see the section "Live Mode" for more details), the canister HTTP outcalls are processed
Expand Down
51 changes: 33 additions & 18 deletions packages/pocket-ic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,43 @@ With PocketIC Rust, testing canisters is as simple as calling Rust functions.
Here is a simple example:

```rust
use candid::encode_one;
use candid::{Principal, encode_one};
use pocket_ic::PocketIc;

#[test]
fn test_counter_canister() {
// 2T cycles
const INIT_CYCLES: u128 = 2_000_000_000_000;

#[test]
fn test_counter_canister() {
let pic = PocketIc::new();
// Create an empty canister as the anonymous principal and add cycles.

// Create a canister and charge it with 2T cycles.
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 2_000_000_000_000);

let wasm_bytes = load_counter_wasm(...);
pic.install_canister(canister_id, wasm_bytes, vec![], None);
// 'inc' is a counter canister method.
call_counter_canister(&pic, canister_id, "inc");
// Check if it had the desired effect.
let reply = call_counter_canister(&pic, canister_id, "read");
assert_eq!(reply, WasmResult::Reply(vec![0, 0, 0, 1]));
}

fn call_counter_canister(pic: &PocketIc, canister_id: CanisterId, method: &str) -> WasmResult {
pic.update_call(canister_id, Principal::anonymous(), method, encode_one(()).unwrap())
.expect("Failed to call counter canister")
pic.add_cycles(canister_id, INIT_CYCLES);

// Install the counter canister wasm file on the canister.
let counter_wasm = todo!();
pic.install_canister(canister_id, counter_wasm, vec![], None);

// Make some calls to the canister.
let reply = call_counter_can(&pic, canister_id, "read");
assert_eq!(reply, vec![0, 0, 0, 0]);
let reply = call_counter_can(&pic, canister_id, "write");
assert_eq!(reply, vec![1, 0, 0, 0]);
let reply = call_counter_can(&pic, canister_id, "write");
assert_eq!(reply, vec![2, 0, 0, 0]);
let reply = call_counter_can(&pic, canister_id, "read");
assert_eq!(reply, vec![2, 0, 0, 0]);
}

fn call_counter_can(pic: &PocketIc, canister_id: Principal, method: &str) -> Vec<u8> {
pic.update_call(
canister_id,
Principal::anonymous(),
method,
encode_one(()).unwrap(),
)
.expect("Failed to call counter canister")
}
```

Expand Down
39 changes: 21 additions & 18 deletions packages/pocket-ic/src/common/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! The types in this module are used to serialize and deserialize data
//! from and to JSON, and are used by both crates.

use crate::UserError;
use crate::RejectResponse;
use candid::Principal;
use hex;
use reqwest::Response;
Expand Down Expand Up @@ -128,12 +128,6 @@ pub struct RawIngressStatusArgs {
pub raw_caller: Option<RawPrincipalId>,
}

#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
pub enum RawSubmitIngressResult {
Ok(RawMessageId),
Err(UserError),
}

#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
pub struct RawCanisterCall {
#[serde(deserialize_with = "base64::deserialize")]
Expand All @@ -151,21 +145,30 @@ pub struct RawCanisterCall {

#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
pub enum RawCanisterResult {
Ok(RawWasmResult),
Err(UserError),
}

#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
pub enum RawWasmResult {
/// Raw response, returned in a "happy" case
Reply(
Ok(
#[serde(deserialize_with = "base64::deserialize")]
#[serde(serialize_with = "base64::serialize")]
Vec<u8>,
),
/// Returned with an error message when the canister decides to reject the
/// message
Reject(String),
Err(RejectResponse),
}

impl From<Result<Vec<u8>, RejectResponse>> for RawCanisterResult {
fn from(result: Result<Vec<u8>, RejectResponse>) -> Self {
match result {
Ok(data) => RawCanisterResult::Ok(data),
Err(reject_response) => RawCanisterResult::Err(reject_response),
}
}
}

impl From<RawCanisterResult> for Result<Vec<u8>, RejectResponse> {
fn from(result: RawCanisterResult) -> Self {
match result {
RawCanisterResult::Ok(data) => Ok(data),
RawCanisterResult::Err(reject_response) => Err(reject_response),
}
}
}

#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
Expand Down
Loading
Loading