From 6451b06f4668996fc94038a9f3125fa49a4df3b9 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 7 Nov 2025 15:32:45 +0100 Subject: [PATCH 1/8] Add READMEs and examples --- Cargo.lock | 1 + ic-canister-runtime/README.md | 48 ++++++++++++++++++ ic-canister-runtime/src/lib.rs | 26 ++++++++++ ic-mock-http-canister-runtime/Cargo.toml | 1 + ic-mock-http-canister-runtime/README.md | 47 ++++++++++++++++++ ic-mock-http-canister-runtime/src/lib.rs | 49 +++++++++++++++++-- ic-mock-http-canister-runtime/src/mock/mod.rs | 10 ++++ 7 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 ic-canister-runtime/README.md create mode 100644 ic-mock-http-canister-runtime/README.md diff --git a/Cargo.lock b/Cargo.lock index 1f8bc6c..b81ac83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,6 +926,7 @@ dependencies = [ "pocket-ic", "serde", "serde_json", + "tokio", "url", ] diff --git a/ic-canister-runtime/README.md b/ic-canister-runtime/README.md new file mode 100644 index 0000000..a4a12c2 --- /dev/null +++ b/ic-canister-runtime/README.md @@ -0,0 +1,48 @@ +[![Internet Computer portal](https://img.shields.io/badge/InternetComputer-grey?logo=internet%20computer&style=for-the-badge)](https://internetcomputer.org) +[![DFinity Forum](https://img.shields.io/badge/help-post%20on%20forum.dfinity.org-blue?style=for-the-badge)](https://forum.dfinity.org/) +[![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?logo=apache&style=for-the-badge)](LICENSE) + + +# `ic-mock-http-canister-runtime` + +Library to abstract the canister runtime so that code making requests to canisters can be reused, e.g.: +* in production using [`ic_cdk`](https://crates.io/crates/ic-cdk), +* in unit tests by mocking this crate's `Runtime` trait, +* in integration tests by implementing this trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) or using the `MockHttpRuntime` implementation from the [`ic-mock-http-canister-runtime`](https://crates.io/crates/ic-mock-http-canister-runtime) crate. + +## Usage + +Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-canister-runtime) for the latest version): + +```toml +ic-canister-runtime = "0.1.0" +``` + +Then, use the library to abstract your code making requests to canisters as follows: +```rust +use ic_canister_runtime::{IcRuntime, Runtime}; + +let runtime = IcRuntime::new(); + +// Make a request to the `http_request` example canister's `make_http_post_request` endpoint +// See: https://github.com/dfinity/canhttp/tree/main/examples/http_canister +let http_request_result: String = runtime + .update_call(canister_id, "make_http_post_request", (), 0) + .await + .expect("Call to `http_canister` failed"); + +assert!(http_request_result.contains("Hello, World!")); +assert!(http_request_result.contains("\"X-Id\": \"42\"")); +``` + +See the [Rust documentation](https://docs.rs/ic-canister-runtime) for more details. + +## Cargo Features + +### Feature `wallet` + +Provides the `CyclesWalletRuntime` implementation which allows routing update calls to a canister through a [cycles wallet](https://github.com/dfinity/cycles-wallet) to attach cycles to them. + +## License + +This project is licensed under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0). \ No newline at end of file diff --git a/ic-canister-runtime/src/lib.rs b/ic-canister-runtime/src/lib.rs index 734e634..92e53b2 100644 --- a/ic-canister-runtime/src/lib.rs +++ b/ic-canister-runtime/src/lib.rs @@ -117,6 +117,32 @@ impl From for IcError { } /// Runtime when interacting with a canister running on the Internet Computer. +/// +/// # Examples +/// +/// Call the `make_http_post_request` endpoint on the example [`http_canister`]. +/// ```rust +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// use candid::Principal; +/// use ic_canister_runtime::{IcRuntime, Runtime, StubRuntime}; +/// +/// let runtime = IcRuntime::new(); +/// # let runtime = StubRuntime::new() +/// # .add_stub_response(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#); +/// # let canister_id = Principal::anonymous(); +/// let http_request_result: String = runtime +/// .update_call(canister_id, "make_http_post_request", (), 0) +/// .await +/// .expect("Call to `http_canister` failed"); +/// +/// assert!(http_request_result.contains("Hello, World!")); +/// assert!(http_request_result.contains("\"X-Id\": \"42\"")); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] pub struct IcRuntime { _private: (), diff --git a/ic-mock-http-canister-runtime/Cargo.toml b/ic-mock-http-canister-runtime/Cargo.toml index f457fa0..5c56f3d 100644 --- a/ic-mock-http-canister-runtime/Cargo.toml +++ b/ic-mock-http-canister-runtime/Cargo.toml @@ -24,3 +24,4 @@ serde_json = { workspace = true } url = { workspace = true } [dev-dependencies] +tokio = { workspace = true } diff --git a/ic-mock-http-canister-runtime/README.md b/ic-mock-http-canister-runtime/README.md new file mode 100644 index 0000000..46d0a64 --- /dev/null +++ b/ic-mock-http-canister-runtime/README.md @@ -0,0 +1,47 @@ +[![Internet Computer portal](https://img.shields.io/badge/InternetComputer-grey?logo=internet%20computer&style=for-the-badge)](https://internetcomputer.org) +[![DFinity Forum](https://img.shields.io/badge/help-post%20on%20forum.dfinity.org-blue?style=for-the-badge)](https://forum.dfinity.org/) +[![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?logo=apache&style=for-the-badge)](LICENSE) + + +# `ic-mock-http-canister-runtime` + +Library to mock [HTTPs outcalls](https://internetcomputer.org/https-outcalls) on the Internet Computer leveraging the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime) crate's `Runtime` trait as well as [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic). + +## Usage + +Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-mock-http-canister-runtime) for the latest version): + +```toml +ic-mock-http-canister-runtime = "0.1.0" +``` + +Then, use the library to mock HTTP outcalls for canister deployed with PocketIC, as follows: +```rust +use ic_mock_http_canister_runtime::{ + AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, + MockHttpRuntime +}; + +let mocks = MockHttpOutcallsBuilder::new() + .given(AnyCanisterHttpRequestMatcher) + .respond_with( + CanisterHttpReply::with_status(200) + .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) + ); + +let runtime = MockHttpRuntime::new(pocket_ic, Principal::anonymous(), mocks); + +let http_request_result: String = runtime + .update_call(canister_id, "make_http_post_request", (), 0) + .await + .expect("Call to `http_canister` failed"); + +assert!(http_request_result.contains("Hello, World!")); +assert!(http_request_result.contains("\"X-Id\": \"42\"")); +``` + +See the [Rust documentation](https://docs.rs/ic-mock-http-canister-runtime) for more details. + +## License + +This project is licensed under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0). \ No newline at end of file diff --git a/ic-mock-http-canister-runtime/src/lib.rs b/ic-mock-http-canister-runtime/src/lib.rs index 0cd4062..718f796 100644 --- a/ic-mock-http-canister-runtime/src/lib.rs +++ b/ic-mock-http-canister-runtime/src/lib.rs @@ -1,5 +1,5 @@ -//! Library to mock HTTP outcalls on the Internet Computer leveraging the [`ic_canister_runtime`] crate's -//! [`Runtime`] trait as well as [`PocketIc`]. +//! Library to mock HTTP outcalls on the Internet Computer leveraging the [`ic_canister_runtime`] +//! crate's [`Runtime`] trait as well as [`PocketIc`]. #![forbid(unsafe_code)] #![forbid(missing_docs)] @@ -13,8 +13,8 @@ use ic_cdk::call::{CallFailed, CallRejected}; use ic_error_types::RejectCode; pub use mock::{ json::{JsonRpcRequestMatcher, JsonRpcResponse}, - CanisterHttpReject, CanisterHttpReply, CanisterHttpRequestMatcher, MockHttpOutcall, - MockHttpOutcallBuilder, MockHttpOutcalls, MockHttpOutcallsBuilder, + AnyCanisterHttpRequestMatcher, CanisterHttpReject, CanisterHttpReply, CanisterHttpRequestMatcher, + MockHttpOutcall, MockHttpOutcallBuilder, MockHttpOutcalls, MockHttpOutcallsBuilder, }; use pocket_ic::{ common::rest::{CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse}, @@ -34,6 +34,45 @@ const MAX_TICKS: usize = 10; /// /// This runtime allows making calls to canisters through Pocket IC while verifying the HTTP /// outcalls made and mocking their responses. +/// +/// # Examples +/// Call the `make_http_post_request` endpoint on the example [`http_canister`] deployed with +/// Pocket IC and mock the resulting HTTP outcall. +/// ```rust, no_run +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// use ic_mock_http_canister_runtime::{ +/// AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, +/// MockHttpRuntime +/// }; +/// # use candid::Principal; +/// # use ic_canister_runtime::{Runtime, StubRuntime}; +/// # use pocket_ic::nonblocking::PocketIc; +/// # use std::{sync::Arc, mem::MaybeUninit}; +/// +/// # let pocket_ic: Arc = unsafe { Arc::new(unsafe { MaybeUninit::zeroed().assume_init() }) }; +/// let mocks = MockHttpOutcallsBuilder::new() +/// .given(AnyCanisterHttpRequestMatcher) +/// .respond_with( +/// CanisterHttpReply::with_status(200) +/// .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) +/// ); +/// +/// let runtime = MockHttpRuntime::new(pocket_ic, Principal::anonymous(), mocks); +/// # let canister_id = Principal::anonymous(); +/// +/// let http_request_result: String = runtime +/// .update_call(canister_id, "make_http_post_request", (), 0) +/// .await +/// .expect("Call to `http_canister` failed"); +/// +/// assert!(http_request_result.contains("Hello, World!")); +/// assert!(http_request_result.contains("\"X-Id\": \"42\"")); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ pub struct MockHttpRuntime { env: Arc, caller: Principal, @@ -45,7 +84,7 @@ impl MockHttpRuntime { /// All calls to canisters are made using the given caller identity. pub fn new(env: Arc, caller: Principal, mocks: impl Into) -> Self { Self { - env: env.clone(), + env, caller, mocks: Mutex::new(mocks.into()), } diff --git a/ic-mock-http-canister-runtime/src/mock/mod.rs b/ic-mock-http-canister-runtime/src/mock/mod.rs index 5286257..902412e 100644 --- a/ic-mock-http-canister-runtime/src/mock/mod.rs +++ b/ic-mock-http-canister-runtime/src/mock/mod.rs @@ -188,6 +188,16 @@ pub trait CanisterHttpRequestMatcher: Send + Debug { fn matches(&self, request: &CanisterHttpRequest) -> bool; } +/// Implementation of [`CanisterHttpRequestMatcher`] that matches all requests. +#[derive(Debug)] +pub struct AnyCanisterHttpRequestMatcher; + +impl CanisterHttpRequestMatcher for AnyCanisterHttpRequestMatcher { + fn matches(&self, _request: &CanisterHttpRequest) -> bool { + true + } +} + /// A wrapper over [`CanisterHttpReply`] that offers a fluent API to create instances. /// /// # Examples From 9fd03969a289797d0d32c0c539c14740ced7916e Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 7 Nov 2025 15:37:42 +0100 Subject: [PATCH 2/8] Format --- ic-mock-http-canister-runtime/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ic-mock-http-canister-runtime/src/lib.rs b/ic-mock-http-canister-runtime/src/lib.rs index 718f796..87c6a80 100644 --- a/ic-mock-http-canister-runtime/src/lib.rs +++ b/ic-mock-http-canister-runtime/src/lib.rs @@ -13,8 +13,9 @@ use ic_cdk::call::{CallFailed, CallRejected}; use ic_error_types::RejectCode; pub use mock::{ json::{JsonRpcRequestMatcher, JsonRpcResponse}, - AnyCanisterHttpRequestMatcher, CanisterHttpReject, CanisterHttpReply, CanisterHttpRequestMatcher, - MockHttpOutcall, MockHttpOutcallBuilder, MockHttpOutcalls, MockHttpOutcallsBuilder, + AnyCanisterHttpRequestMatcher, CanisterHttpReject, CanisterHttpReply, + CanisterHttpRequestMatcher, MockHttpOutcall, MockHttpOutcallBuilder, MockHttpOutcalls, + MockHttpOutcallsBuilder, }; use pocket_ic::{ common::rest::{CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse}, @@ -46,9 +47,9 @@ const MAX_TICKS: usize = 10; /// MockHttpRuntime /// }; /// # use candid::Principal; -/// # use ic_canister_runtime::{Runtime, StubRuntime}; +/// # use ic_canister_runtime::Runtime; /// # use pocket_ic::nonblocking::PocketIc; -/// # use std::{sync::Arc, mem::MaybeUninit}; +/// # use std::{mem::MaybeUninit, sync::Arc}; /// /// # let pocket_ic: Arc = unsafe { Arc::new(unsafe { MaybeUninit::zeroed().assume_init() }) }; /// let mocks = MockHttpOutcallsBuilder::new() From e43d192ff40a3ce5bf3781350a840d285b8aff5c Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 7 Nov 2025 16:16:40 +0100 Subject: [PATCH 3/8] Typo --- ic-canister-runtime/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ic-canister-runtime/README.md b/ic-canister-runtime/README.md index a4a12c2..d437599 100644 --- a/ic-canister-runtime/README.md +++ b/ic-canister-runtime/README.md @@ -3,7 +3,7 @@ [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?logo=apache&style=for-the-badge)](LICENSE) -# `ic-mock-http-canister-runtime` +# `ic-canister-runtime` Library to abstract the canister runtime so that code making requests to canisters can be reused, e.g.: * in production using [`ic_cdk`](https://crates.io/crates/ic-cdk), From 6195d060d30fbc346b454711ce5d863934c9ba5b Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 10 Nov 2025 10:40:21 +0100 Subject: [PATCH 4/8] Refactor `MockHttpOutcallsRuntime` to `PocketIcRuntime` --- Cargo.lock | 3 +- Cargo.toml | 3 +- ic-canister-runtime/README.md | 2 +- .../CHANGELOG.md | 0 .../Cargo.toml | 7 +- .../LICENSE | 0 .../NOTICE | 0 .../README.md | 14 +- .../src/lib.rs | 152 +++++++++++++----- .../src/mock/json/mod.rs | 0 .../src/mock/json/tests.rs | 0 .../src/mock/mod.rs | 35 ++-- release-plz.toml | 5 + 13 files changed, 148 insertions(+), 73 deletions(-) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/CHANGELOG.md (100%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/Cargo.toml (76%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/LICENSE (100%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/NOTICE (100%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/README.md (71%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/src/lib.rs (56%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/src/mock/json/mod.rs (100%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/src/mock/json/tests.rs (100%) rename {ic-mock-http-canister-runtime => ic-pocket-canister-runtime}/src/mock/mod.rs (91%) diff --git a/Cargo.lock b/Cargo.lock index b81ac83..3c1a68a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -914,7 +914,7 @@ dependencies = [ ] [[package]] -name = "ic-mock-http-canister-runtime" +name = "ic-pocket-canister-runtime" version = "0.1.0" dependencies = [ "async-trait", @@ -923,6 +923,7 @@ dependencies = [ "ic-canister-runtime", "ic-cdk", "ic-error-types", + "once_cell", "pocket-ic", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 04ab532..742067f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "canhttp", "examples/http_canister", "ic-canister-runtime", - "ic-mock-http-canister-runtime", + "ic-pocket-canister-runtime", ] resolver = "2" @@ -32,6 +32,7 @@ ic-test-utilities-load-wasm = { git = "https://github.com/dfinity/ic", tag = "re itertools = "0.14.0" maplit = "1.0.2" num-traits = "0.2.19" +once_cell = "1.21.3" pin-project = "1.1.10" pocket-ic = "9.0.2" proptest = "1.6.0" diff --git a/ic-canister-runtime/README.md b/ic-canister-runtime/README.md index d437599..10c5e1d 100644 --- a/ic-canister-runtime/README.md +++ b/ic-canister-runtime/README.md @@ -8,7 +8,7 @@ Library to abstract the canister runtime so that code making requests to canisters can be reused, e.g.: * in production using [`ic_cdk`](https://crates.io/crates/ic-cdk), * in unit tests by mocking this crate's `Runtime` trait, -* in integration tests by implementing this trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) or using the `MockHttpRuntime` implementation from the [`ic-mock-http-canister-runtime`](https://crates.io/crates/ic-mock-http-canister-runtime) crate. +* in integration tests by implementing this trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) yourself or using the `PokcetIcRuntime` implementation from the [`ic-pocket-canister-runtime`](https://crates.io/crates/ic-pocket-canister-runtime) crate. ## Usage diff --git a/ic-mock-http-canister-runtime/CHANGELOG.md b/ic-pocket-canister-runtime/CHANGELOG.md similarity index 100% rename from ic-mock-http-canister-runtime/CHANGELOG.md rename to ic-pocket-canister-runtime/CHANGELOG.md diff --git a/ic-mock-http-canister-runtime/Cargo.toml b/ic-pocket-canister-runtime/Cargo.toml similarity index 76% rename from ic-mock-http-canister-runtime/Cargo.toml rename to ic-pocket-canister-runtime/Cargo.toml index 5c56f3d..c952be6 100644 --- a/ic-mock-http-canister-runtime/Cargo.toml +++ b/ic-pocket-canister-runtime/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "ic-mock-http-canister-runtime" +name = "ic-pocket-canister-runtime" version = "0.1.0" -description = "Mock HTTP outcalls with Pocket IC" +description = "Canisters runtime on the Internet Computer using Pocket IC" license.workspace = true readme.workspace = true homepage.workspace = true @@ -9,7 +9,7 @@ authors.workspace = true edition.workspace = true include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] repository.workspace = true -documentation = "https://docs.rs/ic-mock-http-canister-runtime" +documentation = "https://docs.rs/ic-pocket-canister-runtime" [dependencies] async-trait = { workspace = true } @@ -18,6 +18,7 @@ canhttp = { workspace = true, features = ["json", "http"] } ic-canister-runtime = { workspace = true } ic-cdk = { workspace = true } ic-error-types = { workspace = true } +once_cell = { workspace = true } pocket-ic = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/ic-mock-http-canister-runtime/LICENSE b/ic-pocket-canister-runtime/LICENSE similarity index 100% rename from ic-mock-http-canister-runtime/LICENSE rename to ic-pocket-canister-runtime/LICENSE diff --git a/ic-mock-http-canister-runtime/NOTICE b/ic-pocket-canister-runtime/NOTICE similarity index 100% rename from ic-mock-http-canister-runtime/NOTICE rename to ic-pocket-canister-runtime/NOTICE diff --git a/ic-mock-http-canister-runtime/README.md b/ic-pocket-canister-runtime/README.md similarity index 71% rename from ic-mock-http-canister-runtime/README.md rename to ic-pocket-canister-runtime/README.md index 46d0a64..18b4618 100644 --- a/ic-mock-http-canister-runtime/README.md +++ b/ic-pocket-canister-runtime/README.md @@ -2,22 +2,22 @@ [![DFinity Forum](https://img.shields.io/badge/help-post%20on%20forum.dfinity.org-blue?style=for-the-badge)](https://forum.dfinity.org/) [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?logo=apache&style=for-the-badge)](LICENSE) +# `ic-pocket-canister-runtime` -# `ic-mock-http-canister-runtime` - -Library to mock [HTTPs outcalls](https://internetcomputer.org/https-outcalls) on the Internet Computer leveraging the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime) crate's `Runtime` trait as well as [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic). +Implementation of the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime) crate's `Runtime` trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) allowing to mock +[HTTPs outcalls](https://internetcomputer.org/https-outcalls). ## Usage -Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-mock-http-canister-runtime) for the latest version): +Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-pocket-canister-runtime) for the latest version): ```toml -ic-mock-http-canister-runtime = "0.1.0" +ic-pocket-canister-runtime = "0.1.0" ``` Then, use the library to mock HTTP outcalls for canister deployed with PocketIC, as follows: ```rust -use ic_mock_http_canister_runtime::{ +use ic_pocket_canister_runtime::{ AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, MockHttpRuntime }; @@ -40,7 +40,7 @@ assert!(http_request_result.contains("Hello, World!")); assert!(http_request_result.contains("\"X-Id\": \"42\"")); ``` -See the [Rust documentation](https://docs.rs/ic-mock-http-canister-runtime) for more details. +See the [Rust documentation](https://docs.rs/ic-pocket-canister-runtime) for more details. ## License diff --git a/ic-mock-http-canister-runtime/src/lib.rs b/ic-pocket-canister-runtime/src/lib.rs similarity index 56% rename from ic-mock-http-canister-runtime/src/lib.rs rename to ic-pocket-canister-runtime/src/lib.rs index 87c6a80..1ef8440 100644 --- a/ic-mock-http-canister-runtime/src/lib.rs +++ b/ic-pocket-canister-runtime/src/lib.rs @@ -23,43 +23,37 @@ use pocket_ic::{ RejectResponse, }; use serde::de::DeserializeOwned; -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +use std::time::Duration; const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; const MAX_TICKS: usize = 10; -/// [`Runtime`] using [`PocketIc`] to mock HTTP outcalls. -/// -/// This runtime allows making calls to canisters through Pocket IC while verifying the HTTP -/// outcalls made and mocking their responses. +#[derive(Default)] +enum PocketIcMode { + Live, + #[default] + NotLive, +} + +/// [`Runtime`] using [`PocketIc`] to make calls to canisters. /// /// # Examples /// Call the `make_http_post_request` endpoint on the example [`http_canister`] deployed with -/// Pocket IC and mock the resulting HTTP outcall. +/// Pocket IC. /// ```rust, no_run /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { -/// use ic_mock_http_canister_runtime::{ +/// use ic_pocket_canister_runtime::{ /// AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, -/// MockHttpRuntime +/// PocketIcRuntime /// }; /// # use candid::Principal; /// # use ic_canister_runtime::Runtime; /// # use pocket_ic::nonblocking::PocketIc; -/// # use std::{mem::MaybeUninit, sync::Arc}; -/// -/// # let pocket_ic: Arc = unsafe { Arc::new(unsafe { MaybeUninit::zeroed().assume_init() }) }; -/// let mocks = MockHttpOutcallsBuilder::new() -/// .given(AnyCanisterHttpRequestMatcher) -/// .respond_with( -/// CanisterHttpReply::with_status(200) -/// .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) -/// ); +/// # use std::mem::MaybeUninit; /// -/// let runtime = MockHttpRuntime::new(pocket_ic, Principal::anonymous(), mocks); +/// # let pocket_ic: PocketIc = unsafe { MaybeUninit::zeroed().assume_init() }; +/// let runtime = PocketIcRuntime::new(&pocket_ic, Principal::anonymous()); /// # let canister_id = Principal::anonymous(); /// /// let http_request_result: String = runtime @@ -74,26 +68,88 @@ const MAX_TICKS: usize = 10; /// ``` /// /// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ -pub struct MockHttpRuntime { - env: Arc, +pub struct PocketIcRuntime<'a> { + env: &'a PocketIc, caller: Principal, - mocks: Mutex, + mocks: Option>, + mode: PocketIcMode, } -impl MockHttpRuntime { - /// Create a new [`MockHttpRuntime`] with the given [`PocketIc`] and [`MockHttpOutcalls`]. +impl<'a> PocketIcRuntime<'a> { + /// Create a new [`PocketIcRuntime`] with the given [`PocketIc`]. /// All calls to canisters are made using the given caller identity. - pub fn new(env: Arc, caller: Principal, mocks: impl Into) -> Self { + pub fn new(env: &'a PocketIc, caller: Principal) -> Self { Self { env, caller, - mocks: Mutex::new(mocks.into()), + mocks: None, + mode: Default::default(), } } + + /// Mock HTTP outcalls and their responses. + /// + /// This allows making calls to canisters through Pocket IC while verifying the HTTP outcalls + /// made and mocking their responses. + /// + /// # Examples + /// Call the `make_http_post_request` endpoint on the example [`http_canister`] deployed with + /// Pocket IC and mock the resulting HTTP outcall. + /// ```rust, no_run + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// use ic_pocket_canister_runtime::{ + /// AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, + /// PocketIcRuntime + /// }; + /// # use candid::Principal; + /// # use ic_canister_runtime::Runtime; + /// # use pocket_ic::nonblocking::PocketIc; + /// # use std::mem::MaybeUninit; + /// + /// # let pocket_ic: PocketIc = unsafe { MaybeUninit::zeroed().assume_init() }; + /// let mocks = MockHttpOutcallsBuilder::new() + /// // Matches any HTTP outcall request + /// .given(AnyCanisterHttpRequestMatcher) + /// // Assert that the HTTP outcall response has the given status code and body + /// .respond_with( + /// CanisterHttpReply::with_status(200) + /// .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) + /// ); + /// + /// let runtime = PocketIcRuntime::new(&pocket_ic, Principal::anonymous()) + /// .with_http_mocks(mocks.build()); + /// # let canister_id = Principal::anonymous(); + /// + /// let http_request_result: String = runtime + /// .update_call(canister_id, "make_http_post_request", (), 0) + /// .await + /// .expect("Call to `http_canister` failed"); + /// + /// assert!(http_request_result.contains("Hello, World!")); + /// assert!(http_request_result.contains("\"X-Id\": \"42\"")); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ + pub fn with_http_mocks(mut self, mocks: impl ExecuteHttpOutcallMocks + 'static) -> Self { + self.mocks = Some(Box::new(mocks)); + self + } + + /// Use Pocket IC in [live mode](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43). + /// + /// The pocket IC instance will automatically progress and execute HTTPs outcalls. + /// This setting renders the tests non-deterministic. + pub fn live_mode(mut self) -> Self { + self.mode = PocketIcMode::Live; + self + } } #[async_trait] -impl Runtime for MockHttpRuntime { +impl Runtime for PocketIcRuntime<'_> { async fn update_call( &self, id: Principal, @@ -114,13 +170,16 @@ impl Runtime for MockHttpRuntime { encode_args(args).unwrap_or_else(panic_when_encode_fails), ) .await - .unwrap(); - self.execute_mocks().await; - self.env - .await_call(message_id) - .await - .map(decode_call_response) - .map_err(parse_reject_response)? + .map_err(parse_reject_response)?; + if let Some(mock) = &self.mocks { + mock.execute_http_outcall_mocks(self.env).await; + } + (match self.mode { + PocketIcMode::NotLive => self.env.await_call(message_id).await, + PocketIcMode::Live => self.env.await_call_no_ticks(message_id).await, + }) + .map(decode_call_response) + .map_err(parse_reject_response)? } async fn query_call( @@ -146,15 +205,20 @@ impl Runtime for MockHttpRuntime { } } -impl MockHttpRuntime { - async fn execute_mocks(&self) { +/// Execute HTTP outcall mocks. +#[async_trait] +pub trait ExecuteHttpOutcallMocks: Send + Sync { + /// Execute HTTP outcall mocks. + async fn execute_http_outcall_mocks(&self, runtime: &PocketIc) -> (); +} + +#[async_trait] +impl ExecuteHttpOutcallMocks for MockHttpOutcalls { + async fn execute_http_outcall_mocks(&self, env: &PocketIc) -> () { loop { - let pending_requests = tick_until_http_requests(self.env.as_ref()).await; + let pending_requests = tick_until_http_requests(env).await; if let Some(request) = pending_requests.first() { - let maybe_mock = { - let mut mocks = self.mocks.lock().unwrap(); - mocks.pop_matching(request) - }; + let maybe_mock = { self.pop_matching(request) }; match maybe_mock { Some(mock) => { let mock_response = MockCanisterHttpResponse { @@ -163,7 +227,7 @@ impl MockHttpRuntime { response: check_response_size(request, mock.response), additional_responses: vec![], }; - self.env.mock_canister_http_response(mock_response).await; + env.mock_canister_http_response(mock_response).await; } None => { panic!("No mocks matching the request: {:?}", request); diff --git a/ic-mock-http-canister-runtime/src/mock/json/mod.rs b/ic-pocket-canister-runtime/src/mock/json/mod.rs similarity index 100% rename from ic-mock-http-canister-runtime/src/mock/json/mod.rs rename to ic-pocket-canister-runtime/src/mock/json/mod.rs diff --git a/ic-mock-http-canister-runtime/src/mock/json/tests.rs b/ic-pocket-canister-runtime/src/mock/json/tests.rs similarity index 100% rename from ic-mock-http-canister-runtime/src/mock/json/tests.rs rename to ic-pocket-canister-runtime/src/mock/json/tests.rs diff --git a/ic-mock-http-canister-runtime/src/mock/mod.rs b/ic-pocket-canister-runtime/src/mock/mod.rs similarity index 91% rename from ic-mock-http-canister-runtime/src/mock/mod.rs rename to ic-pocket-canister-runtime/src/mock/mod.rs index 902412e..8a4b716 100644 --- a/ic-mock-http-canister-runtime/src/mock/mod.rs +++ b/ic-pocket-canister-runtime/src/mock/mod.rs @@ -1,6 +1,6 @@ use pocket_ic::common::rest::{CanisterHttpHeader, CanisterHttpRequest, CanisterHttpResponse}; use serde_json::Value; -use std::fmt::Debug; +use std::{fmt::Debug, sync::Mutex}; pub mod json; @@ -9,23 +9,25 @@ pub mod json; /// When an instance of [`MockHttpOutcalls`] is dropped, it panics if not all mocks were /// consumed (i.e., if it is not empty). #[derive(Debug, Default)] -pub struct MockHttpOutcalls(Vec); +pub struct MockHttpOutcalls(Mutex>); impl MockHttpOutcalls { /// Asserts that no HTTP outcalls are performed. - pub const NEVER: MockHttpOutcalls = Self(Vec::new()); + pub fn never() -> MockHttpOutcalls { + MockHttpOutcalls(Mutex::new(Vec::new())) + } /// Add a new mocked HTTP outcall. - pub fn push(&mut self, mock: MockHttpOutcall) { - self.0.push(mock); + pub fn push(&self, mock: MockHttpOutcall) { + self.0.try_lock().unwrap().push(mock); } /// Returns a matching [`MockHttpOutcall`] for the given request if there is one, otherwise /// [`None`]. /// Panics if there are more than one matching [`MockHttpOutcall`]s for the given request. - pub fn pop_matching(&mut self, request: &CanisterHttpRequest) -> Option { - let matching_positions = self - .0 + pub fn pop_matching(&self, request: &CanisterHttpRequest) -> Option { + let mut outcalls = self.0.try_lock().unwrap(); + let matching_positions = outcalls .iter() .enumerate() .filter_map(|(i, mock)| { @@ -39,7 +41,7 @@ impl MockHttpOutcalls { match matching_positions.len() { 0 => None, - 1 => Some(self.0.swap_remove(matching_positions[0])), + 1 => Some(outcalls.swap_remove(matching_positions[0])), _ => panic!("Multiple mocks match the request: {:?}", request), } } @@ -47,10 +49,11 @@ impl MockHttpOutcalls { impl Drop for MockHttpOutcalls { fn drop(&mut self) { - if !self.0.is_empty() { + let outcalls = self.0.try_lock().unwrap(); + if !outcalls.is_empty() { panic!( "MockHttpOutcalls dropped but {} mocks were not consumed: {:?}", - self.0.len(), + outcalls.len(), self.0 ); } @@ -83,7 +86,7 @@ impl MockHttpOutcallsBuilder { /// # Examples /// /// ```rust - /// use ic_mock_http_canister_runtime::{ + /// use ic_pocket_canister_runtime::{ /// CanisterHttpReply, JsonRpcRequestMatcher, MockHttpOutcallsBuilder /// }; /// @@ -146,7 +149,7 @@ impl MockHttpOutcallBuilder { /// # Examples /// /// ```rust - /// use ic_mock_http_canister_runtime::{ + /// use ic_pocket_canister_runtime::{ /// CanisterHttpReply, JsonRpcRequestMatcher, MockHttpOutcallsBuilder /// }; /// @@ -171,7 +174,7 @@ impl MockHttpOutcallBuilder { /// /// [`given`]: MockHttpOutcallsBuilder::given pub fn respond_with( - mut self, + self, response: impl Into, ) -> MockHttpOutcallsBuilder { self.parent.0.push(MockHttpOutcall { @@ -203,7 +206,7 @@ impl CanisterHttpRequestMatcher for AnyCanisterHttpRequestMatcher { /// # Examples /// /// ```rust -/// use ic_mock_http_canister_runtime::CanisterHttpReply; +/// use ic_pocket_canister_runtime::CanisterHttpReply; /// use pocket_ic::common::rest::{CanisterHttpHeader, CanisterHttpResponse}; /// use serde_json::json; /// @@ -281,7 +284,7 @@ impl From for CanisterHttpResponse { /// /// ```rust /// use ic_error_types::RejectCode; -/// use ic_mock_http_canister_runtime::CanisterHttpReject; +/// use ic_pocket_canister_runtime::CanisterHttpReject; /// use pocket_ic::common::rest::CanisterHttpResponse; /// /// let response: CanisterHttpResponse = CanisterHttpReject::with_reject_code(RejectCode::SysTransient) diff --git a/release-plz.toml b/release-plz.toml index 2aa344d..9c549ce 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -39,6 +39,11 @@ name = "ic-canister-runtime" #git_release_enable = false # enable GitHub releases publish = true # enable `cargo publish` +[[package]] +name = "ic-pocket-runtime" +#git_release_enable = false # enable GitHub releases +publish = true # enable `cargo publish` + [[package]] name = "http_canister" release = false # don't process this package \ No newline at end of file From abdd9944fc3edb48bc71c1ded3d022a26573652a Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 17 Nov 2025 13:50:18 +0100 Subject: [PATCH 5/8] Check if `env` is already in live mode --- ic-pocket-canister-runtime/src/lib.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/ic-pocket-canister-runtime/src/lib.rs b/ic-pocket-canister-runtime/src/lib.rs index 5524513..387b1f8 100644 --- a/ic-pocket-canister-runtime/src/lib.rs +++ b/ic-pocket-canister-runtime/src/lib.rs @@ -144,11 +144,27 @@ impl<'a> PocketIcRuntime<'a> { self } - /// Use Pocket IC in [live mode](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43). + /// Configures this [`PocketIcRuntime`] instance to use [`PocketIc`] in live mode. /// - /// The pocket IC instance will automatically progress and execute HTTPs outcalls. - /// This setting renders the tests non-deterministic. + /// In live mode, PocketIC automatically progresses time and executes outgoing HTTPS requests + /// as they occur. + /// This makes test execution **non-deterministic**. + /// + /// **Important:** + /// This method **does not** switch the underlying [`PocketIc`] instance to live mode. + /// You must first call [`PocketIc::make_live`] to do so. + /// + /// # Panics + /// Panics if the underlying [`PocketIc`] instance is not in live mode + /// (i.e., if [`PocketIc::make_live`] was not called beforehand). + /// + /// # See also + /// - [PocketIC live mode documentation](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43) pub fn live_mode(mut self) -> Self { + assert!( + self.env.auto_progress_enabled(), + "Auto-progress must be enabled on `PocketIc` instance with `PocketIc::make_live()`" + ); self.mode = PocketIcMode::Live; self } From 96b063bbc95b53874f92177face28c1f7958237a Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 18 Nov 2025 13:07:29 +0100 Subject: [PATCH 6/8] Move `Mutex` to `PocketIcRuntime` --- Cargo.lock | 1 - ic-pocket-canister-runtime/Cargo.toml | 5 +--- ic-pocket-canister-runtime/src/lib.rs | 21 +++++++++++------ ic-pocket-canister-runtime/src/mock/mod.rs | 27 +++++++++++----------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6298308..f48f73b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1318,7 +1318,6 @@ dependencies = [ "ic-canister-runtime", "ic-cdk", "ic-error-types", - "once_cell", "pocket-ic", "serde", "serde_json", diff --git a/ic-pocket-canister-runtime/Cargo.toml b/ic-pocket-canister-runtime/Cargo.toml index c952be6..fefdc7e 100644 --- a/ic-pocket-canister-runtime/Cargo.toml +++ b/ic-pocket-canister-runtime/Cargo.toml @@ -18,11 +18,8 @@ canhttp = { workspace = true, features = ["json", "http"] } ic-canister-runtime = { workspace = true } ic-cdk = { workspace = true } ic-error-types = { workspace = true } -once_cell = { workspace = true } pocket-ic = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -url = { workspace = true } - -[dev-dependencies] tokio = { workspace = true } +url = { workspace = true } diff --git a/ic-pocket-canister-runtime/src/lib.rs b/ic-pocket-canister-runtime/src/lib.rs index 387b1f8..f01d646 100644 --- a/ic-pocket-canister-runtime/src/lib.rs +++ b/ic-pocket-canister-runtime/src/lib.rs @@ -24,6 +24,7 @@ use pocket_ic::{ }; use serde::de::DeserializeOwned; use std::time::Duration; +use tokio::sync::Mutex; const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; const MAX_TICKS: usize = 10; @@ -78,7 +79,10 @@ enum PocketIcMode { pub struct PocketIcRuntime<'a> { env: &'a PocketIc, caller: Principal, - mocks: Option>, + // The mocks are stored in a Mutex> so they can be modified in the implementation of + // the `Runtime::update_call` method using interior mutability. + // This is necessary since `Runtime::update_call` takes an immutable reference to the runtime. + mocks: Option>>, mode: PocketIcMode, } @@ -140,7 +144,7 @@ impl<'a> PocketIcRuntime<'a> { /// /// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ pub fn with_http_mocks(mut self, mocks: impl ExecuteHttpOutcallMocks + 'static) -> Self { - self.mocks = Some(Box::new(mocks)); + self.mocks = Some(Mutex::new(Box::new(mocks))); self } @@ -160,9 +164,9 @@ impl<'a> PocketIcRuntime<'a> { /// /// # See also /// - [PocketIC live mode documentation](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43) - pub fn live_mode(mut self) -> Self { + pub async fn live_mode(mut self) -> Self { assert!( - self.env.auto_progress_enabled(), + self.env.auto_progress_enabled().await, "Auto-progress must be enabled on `PocketIc` instance with `PocketIc::make_live()`" ); self.mode = PocketIcMode::Live; @@ -194,7 +198,10 @@ impl Runtime for PocketIcRuntime<'_> { .await .map_err(parse_reject_response)?; if let Some(mock) = &self.mocks { - mock.execute_http_outcall_mocks(self.env).await; + mock.try_lock() + .unwrap() + .execute_http_outcall_mocks(self.env) + .await; } (match self.mode { PocketIcMode::NotLive => self.env.await_call(message_id).await, @@ -231,12 +238,12 @@ impl Runtime for PocketIcRuntime<'_> { #[async_trait] pub trait ExecuteHttpOutcallMocks: Send + Sync { /// Execute HTTP outcall mocks. - async fn execute_http_outcall_mocks(&self, runtime: &PocketIc) -> (); + async fn execute_http_outcall_mocks(&mut self, runtime: &PocketIc) -> (); } #[async_trait] impl ExecuteHttpOutcallMocks for MockHttpOutcalls { - async fn execute_http_outcall_mocks(&self, env: &PocketIc) -> () { + async fn execute_http_outcall_mocks(&mut self, env: &PocketIc) -> () { loop { let pending_requests = tick_until_http_requests(env).await; if let Some(request) = pending_requests.first() { diff --git a/ic-pocket-canister-runtime/src/mock/mod.rs b/ic-pocket-canister-runtime/src/mock/mod.rs index 8a4b716..769965f 100644 --- a/ic-pocket-canister-runtime/src/mock/mod.rs +++ b/ic-pocket-canister-runtime/src/mock/mod.rs @@ -1,6 +1,6 @@ use pocket_ic::common::rest::{CanisterHttpHeader, CanisterHttpRequest, CanisterHttpResponse}; use serde_json::Value; -use std::{fmt::Debug, sync::Mutex}; +use std::fmt::Debug; pub mod json; @@ -9,25 +9,25 @@ pub mod json; /// When an instance of [`MockHttpOutcalls`] is dropped, it panics if not all mocks were /// consumed (i.e., if it is not empty). #[derive(Debug, Default)] -pub struct MockHttpOutcalls(Mutex>); +pub struct MockHttpOutcalls(Vec); impl MockHttpOutcalls { /// Asserts that no HTTP outcalls are performed. pub fn never() -> MockHttpOutcalls { - MockHttpOutcalls(Mutex::new(Vec::new())) + MockHttpOutcalls(Vec::new()) } /// Add a new mocked HTTP outcall. - pub fn push(&self, mock: MockHttpOutcall) { - self.0.try_lock().unwrap().push(mock); + pub fn push(&mut self, mock: MockHttpOutcall) { + self.0.push(mock); } /// Returns a matching [`MockHttpOutcall`] for the given request if there is one, otherwise /// [`None`]. /// Panics if there are more than one matching [`MockHttpOutcall`]s for the given request. - pub fn pop_matching(&self, request: &CanisterHttpRequest) -> Option { - let mut outcalls = self.0.try_lock().unwrap(); - let matching_positions = outcalls + pub fn pop_matching(&mut self, request: &CanisterHttpRequest) -> Option { + let matching_positions = self + .0 .iter() .enumerate() .filter_map(|(i, mock)| { @@ -41,7 +41,7 @@ impl MockHttpOutcalls { match matching_positions.len() { 0 => None, - 1 => Some(outcalls.swap_remove(matching_positions[0])), + 1 => Some(self.0.swap_remove(matching_positions[0])), _ => panic!("Multiple mocks match the request: {:?}", request), } } @@ -49,11 +49,10 @@ impl MockHttpOutcalls { impl Drop for MockHttpOutcalls { fn drop(&mut self) { - let outcalls = self.0.try_lock().unwrap(); - if !outcalls.is_empty() { + if !self.0.is_empty() { panic!( "MockHttpOutcalls dropped but {} mocks were not consumed: {:?}", - outcalls.len(), + self.0.len(), self.0 ); } @@ -174,7 +173,7 @@ impl MockHttpOutcallBuilder { /// /// [`given`]: MockHttpOutcallsBuilder::given pub fn respond_with( - self, + mut self, response: impl Into, ) -> MockHttpOutcallsBuilder { self.parent.0.push(MockHttpOutcall { @@ -186,7 +185,7 @@ impl MockHttpOutcallBuilder { } /// A trait that allows checking if a given [`CanisterHttpRequest`] matches an HTTP outcall mock. -pub trait CanisterHttpRequestMatcher: Send + Debug { +pub trait CanisterHttpRequestMatcher: Send + Sync + Debug { /// Returns whether the given [`CanisterHttpRequest`] matches. fn matches(&self, request: &CanisterHttpRequest) -> bool; } From 8667ba32cda1a96067a4f42ef0fdf547d6637377 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 19 Nov 2025 10:24:42 +0100 Subject: [PATCH 7/8] Add constructor for live mode and remove `live_mode` method --- ic-pocket-canister-runtime/src/lib.rs | 78 ++++++++++++++++----------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/ic-pocket-canister-runtime/src/lib.rs b/ic-pocket-canister-runtime/src/lib.rs index f01d646..4ecd876 100644 --- a/ic-pocket-canister-runtime/src/lib.rs +++ b/ic-pocket-canister-runtime/src/lib.rs @@ -30,10 +30,10 @@ const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; const MAX_TICKS: usize = 10; #[derive(Default)] -enum PocketIcMode { - Live, +enum PocketIcLiveMode { + Enabled, #[default] - NotLive, + Disabled, } /// [`Runtime`] using [`PocketIc`] to make calls to canisters. @@ -61,6 +61,7 @@ enum PocketIcMode { /// /// let pocket_ic = PocketIc::new().await; /// let runtime = PocketIcRuntime::new(&pocket_ic, Principal::anonymous()) +/// .await /// .with_http_mocks(mocks.build()); /// # let canister_id = Principal::anonymous(); /// @@ -83,13 +84,21 @@ pub struct PocketIcRuntime<'a> { // the `Runtime::update_call` method using interior mutability. // This is necessary since `Runtime::update_call` takes an immutable reference to the runtime. mocks: Option>>, - mode: PocketIcMode, + mode: PocketIcLiveMode, } impl<'a> PocketIcRuntime<'a> { /// Create a new [`PocketIcRuntime`] with the given [`PocketIc`]. /// All calls to canisters are made using the given caller identity. - pub fn new(env: &'a PocketIc, caller: Principal) -> Self { + /// + /// The given [`PocketIc`] instance must not be configured to use live mode. + /// To build a [`PocketIcRuntime`] instance with [`PocketIc`] running in live mode, + /// see [`PocketIcRuntime::with_live_mode`]. + pub async fn new(env: &'a PocketIc, caller: Principal) -> Self { + assert!( + !env.auto_progress_enabled().await, + "Auto-progress is be enabled on `PocketIc`. To use `PocketIc` in live mode, use `PocketIcRuntime::with_live_mode`" + ); Self { env, caller, @@ -98,6 +107,35 @@ impl<'a> PocketIcRuntime<'a> { } } + /// Create a new [`PocketIcRuntime`] with the given [`PocketIc`] configured to use live mode. + /// + /// In live mode, PocketIC automatically progresses time and executes outgoing HTTPS requests + /// as they occur. + /// This makes test execution **non-deterministic**. + /// + /// **Important:** + /// This method **does not** switch the underlying [`PocketIc`] instance to live mode. + /// You must first call [`PocketIc::make_live`] to do so. + /// + /// # Panics + /// Panics if the underlying [`PocketIc`] instance is not in live mode + /// (i.e., if [`PocketIc::make_live`] was not called beforehand). + /// + /// # See also + /// - [PocketIC live mode documentation](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43) + pub async fn with_live_mode(env: &'a PocketIc, caller: Principal) -> Self { + assert!( + env.auto_progress_enabled().await, + "Auto-progress must be enabled on `PocketIc` instance with `PocketIc::make_live()`" + ); + Self { + env, + caller, + mocks: None, + mode: PocketIcLiveMode::Enabled, + } + } + /// Mock HTTP outcalls and their responses. /// /// This allows making calls to canisters through Pocket IC while verifying the HTTP outcalls @@ -128,6 +166,7 @@ impl<'a> PocketIcRuntime<'a> { /// /// let pocket_ic = PocketIc::new().await; /// let runtime = PocketIcRuntime::new(&pocket_ic, Principal::anonymous()) + /// .await /// .with_http_mocks(mocks.build()); /// # let canister_id = Principal::anonymous(); /// @@ -147,31 +186,6 @@ impl<'a> PocketIcRuntime<'a> { self.mocks = Some(Mutex::new(Box::new(mocks))); self } - - /// Configures this [`PocketIcRuntime`] instance to use [`PocketIc`] in live mode. - /// - /// In live mode, PocketIC automatically progresses time and executes outgoing HTTPS requests - /// as they occur. - /// This makes test execution **non-deterministic**. - /// - /// **Important:** - /// This method **does not** switch the underlying [`PocketIc`] instance to live mode. - /// You must first call [`PocketIc::make_live`] to do so. - /// - /// # Panics - /// Panics if the underlying [`PocketIc`] instance is not in live mode - /// (i.e., if [`PocketIc::make_live`] was not called beforehand). - /// - /// # See also - /// - [PocketIC live mode documentation](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43) - pub async fn live_mode(mut self) -> Self { - assert!( - self.env.auto_progress_enabled().await, - "Auto-progress must be enabled on `PocketIc` instance with `PocketIc::make_live()`" - ); - self.mode = PocketIcMode::Live; - self - } } #[async_trait] @@ -204,8 +218,8 @@ impl Runtime for PocketIcRuntime<'_> { .await; } (match self.mode { - PocketIcMode::NotLive => self.env.await_call(message_id).await, - PocketIcMode::Live => self.env.await_call_no_ticks(message_id).await, + PocketIcLiveMode::Disabled => self.env.await_call(message_id).await, + PocketIcLiveMode::Enabled => self.env.await_call_no_ticks(message_id).await, }) .map(decode_call_response) .map_err(parse_reject_response)? From 24e039055407e167470981f8789cabaebccef1d4 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 19 Nov 2025 10:27:14 +0100 Subject: [PATCH 8/8] Fix package name in `release-plz.toml` --- release-plz.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/release-plz.toml b/release-plz.toml index 94f7398..c9fd70e 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -45,10 +45,18 @@ name = "ic-canister-runtime" publish = true # enable `cargo publish` [[package]] -name = "ic-pocket-runtime" +name = "ic-pocket-canister-runtime" #git_release_enable = false # enable GitHub releases publish = true # enable `cargo publish` [[package]] name = "http_canister" +release = false # don't process this package + +[[package]] +name = "json_rpc_canister" +release = false # don't process this package + +[[package]] +name = "multi_canister" release = false # don't process this package \ No newline at end of file