Skip to content

Commit

Permalink
feat(PocketIC): new call response types (#3425)
Browse files Browse the repository at this point in the history
This PR implements new PocketIC call response types following this
[forum](https://forum.dfinity.org/t/pocketic-call-response-types/38728)
proposal.
  • Loading branch information
mraszyk authored Jan 16, 2025
1 parent 7893a83 commit 2c79ddc
Show file tree
Hide file tree
Showing 24 changed files with 697 additions and 849 deletions.
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

0 comments on commit 2c79ddc

Please sign in to comment.