From 3047b88b07b5cc511b1656670c3ffe35bc981552 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 17 Feb 2026 11:25:26 +0100 Subject: [PATCH 1/6] feat: support for `StubRuntime` returning `IcError` --- ic-canister-runtime/src/stub/mod.rs | 31 ++++++++++++++++----------- ic-canister-runtime/src/stub/tests.rs | 30 +++++++++++++++++++++----- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/ic-canister-runtime/src/stub/mod.rs b/ic-canister-runtime/src/stub/mod.rs index 846c75f..4ea808b 100644 --- a/ic-canister-runtime/src/stub/mod.rs +++ b/ic-canister-runtime/src/stub/mod.rs @@ -26,7 +26,7 @@ use std::{collections::VecDeque, sync::Mutex}; /// let runtime = StubRuntime::new() /// .add_stub_response(1_u64) /// .add_stub_response("two") -/// .add_stub_response(Some(3_u128)); +/// .add_stub_error(IcError::CallRejected); /// /// let result_1: Result = runtime /// .update_call(PRINCIPAL, METHOD, ARGS, 0) @@ -39,31 +39,39 @@ use std::{collections::VecDeque, sync::Mutex}; /// assert_eq!(result_2, Ok("two".to_string())); /// /// let result_3: Result, IcError> = runtime -/// .update_call(PRINCIPAL, METHOD, ARGS, 0) +/// .query_call(PRINCIPAL, METHOD, ARGS) /// .await; -/// assert_eq!(result_3, Ok(Some -/// (3_u128))); +/// assert_eq!(result_3, Err(IcError::CallRejected); /// # Ok(()) /// # } /// ``` #[derive(Debug, Default, Clone)] pub struct StubRuntime { // Use a mutex so that this struct is Send and Sync - call_results: Arc>>>, + call_results: Arc, IcError>>>>, } impl StubRuntime { - /// Create a new empty [`StubRuntime`]. + /// Create a new empty [`ic_canister_runtime::StubRuntime`]. pub fn new() -> Self { Self::default() } - /// Mutate the [`StubRuntime`] instance to add the given stub response. + /// Mutate the [`ic_canister_runtime::StubRuntime`] instance to add the given stub response. /// /// Panics if the stub response cannot be encoded using Candid. pub fn add_stub_response(self, stub_response: Out) -> Self { let result = Encode!(&stub_response).expect("Failed to encode Candid stub response"); - self.call_results.try_lock().unwrap().push_back(result); + self.call_results.try_lock().unwrap().push_back(Ok(result)); + self + } + + /// Mutate the [`ic_canister_runtime::StubRuntime`] instance to add the given stub error. + pub fn add_stub_error(self, stub_error: impl Into) -> Self { + self.call_results + .try_lock() + .unwrap() + .push_back(Err(stub_error.into())); self } @@ -71,13 +79,12 @@ impl StubRuntime { where Out: CandidType + DeserializeOwned, { - let bytes = self - .call_results + self.call_results .try_lock() .unwrap() .pop_front() - .unwrap_or_else(|| panic!("No available call response")); - Ok(Decode!(&bytes, Out).expect("Failed to decode Candid stub response")) + .unwrap_or_else(|| panic!("No available call response")) + .map(|bytes| Decode!(&bytes, Out).expect("Failed to decode Candid stub response")) } } diff --git a/ic-canister-runtime/src/stub/tests.rs b/ic-canister-runtime/src/stub/tests.rs index 75ee108..96a0021 100644 --- a/ic-canister-runtime/src/stub/tests.rs +++ b/ic-canister-runtime/src/stub/tests.rs @@ -1,5 +1,6 @@ use crate::{IcError, Runtime, StubRuntime}; use candid::{CandidType, Principal}; +use ic_error_types::RejectCode; use serde::Deserialize; const DEFAULT_PRINCIPAL: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]); @@ -38,6 +39,18 @@ async fn should_return_single_stub_response() { assert_eq!(result, Ok(expected)); } +#[tokio::test] +async fn should_return_single_stub_error() { + let expected = IcError::CallPerformFailed; + let runtime = StubRuntime::new().add_stub_error(expected.clone()); + + let result: Result = runtime + .update_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS, 0) + .await; + + assert_eq!(result, Err(expected)); +} + #[tokio::test] async fn should_return_multiple_stub_responses() { let expected1 = MultiResult::Consistent("Hello, world!".to_string()); @@ -46,25 +59,32 @@ async fn should_return_multiple_stub_responses() { "Goodbye, world!".to_string(), ]); let expected3 = 0_u128; + let expected4 = IcError::CallRejected { + code: RejectCode::SysFatal, + message: "Fatal error!".to_string(), + }; let runtime = StubRuntime::new() .add_stub_response(expected1.clone()) .add_stub_response(expected2.clone()) - .add_stub_response(expected3); + .add_stub_response(expected3) + .add_stub_error(expected4.clone()); let result1: Result = runtime .update_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS, 0) .await; assert_eq!(result1, Ok(expected1)); - let result2: Result = runtime - .update_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS, 0) + .query_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS) .await; assert_eq!(result2, Ok(expected2)); - let result3: Result = runtime - .update_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS, 0) + .query_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS) .await; assert_eq!(result3, Ok(expected3)); + let result3: Result = runtime + .update_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS, 0) + .await; + assert_eq!(result3, Err(expected4)); } #[tokio::test] From a6b2e9a3f257415df18619db1d4d6fb0fe314aa1 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 17 Feb 2026 11:29:03 +0100 Subject: [PATCH 2/6] Clippy --- ic-canister-runtime/src/stub/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/ic-canister-runtime/src/stub/mod.rs b/ic-canister-runtime/src/stub/mod.rs index 4ea808b..9673d0e 100644 --- a/ic-canister-runtime/src/stub/mod.rs +++ b/ic-canister-runtime/src/stub/mod.rs @@ -48,6 +48,7 @@ use std::{collections::VecDeque, sync::Mutex}; #[derive(Debug, Default, Clone)] pub struct StubRuntime { // Use a mutex so that this struct is Send and Sync + #[allow(clippy::type_complexity)] call_results: Arc, IcError>>>>, } From c3600a0445354a32a58d9be92a7d1f0b5f5a6774 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 17 Feb 2026 11:30:03 +0100 Subject: [PATCH 3/6] Missing bracket --- ic-canister-runtime/src/stub/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ic-canister-runtime/src/stub/mod.rs b/ic-canister-runtime/src/stub/mod.rs index 9673d0e..ab87ae2 100644 --- a/ic-canister-runtime/src/stub/mod.rs +++ b/ic-canister-runtime/src/stub/mod.rs @@ -41,7 +41,7 @@ use std::{collections::VecDeque, sync::Mutex}; /// let result_3: Result, IcError> = runtime /// .query_call(PRINCIPAL, METHOD, ARGS) /// .await; -/// assert_eq!(result_3, Err(IcError::CallRejected); +/// assert_eq!(result_3, Err(IcError::CallRejected)); /// # Ok(()) /// # } /// ``` From 4bc50c39b367c6b69569781b046360a7e0e12de1 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 17 Feb 2026 11:30:36 +0100 Subject: [PATCH 4/6] Fix error --- ic-canister-runtime/src/stub/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ic-canister-runtime/src/stub/mod.rs b/ic-canister-runtime/src/stub/mod.rs index ab87ae2..6709560 100644 --- a/ic-canister-runtime/src/stub/mod.rs +++ b/ic-canister-runtime/src/stub/mod.rs @@ -26,7 +26,7 @@ use std::{collections::VecDeque, sync::Mutex}; /// let runtime = StubRuntime::new() /// .add_stub_response(1_u64) /// .add_stub_response("two") -/// .add_stub_error(IcError::CallRejected); +/// .add_stub_error(IcError::CallPerformFailed); /// /// let result_1: Result = runtime /// .update_call(PRINCIPAL, METHOD, ARGS, 0) @@ -41,7 +41,7 @@ use std::{collections::VecDeque, sync::Mutex}; /// let result_3: Result, IcError> = runtime /// .query_call(PRINCIPAL, METHOD, ARGS) /// .await; -/// assert_eq!(result_3, Err(IcError::CallRejected)); +/// assert_eq!(result_3, Err(IcError::CallPerformFailed)); /// # Ok(()) /// # } /// ``` From 3a99c0540a79bc6c99022c7ba5e29f47bc56de50 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 17 Feb 2026 11:51:10 +0100 Subject: [PATCH 5/6] Fix Rustdoc --- ic-canister-runtime/src/stub/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ic-canister-runtime/src/stub/mod.rs b/ic-canister-runtime/src/stub/mod.rs index 6709560..4b92894 100644 --- a/ic-canister-runtime/src/stub/mod.rs +++ b/ic-canister-runtime/src/stub/mod.rs @@ -53,12 +53,12 @@ pub struct StubRuntime { } impl StubRuntime { - /// Create a new empty [`ic_canister_runtime::StubRuntime`]. + /// Create a new empty [`StubRuntime`]. pub fn new() -> Self { Self::default() } - /// Mutate the [`ic_canister_runtime::StubRuntime`] instance to add the given stub response. + /// Mutate the [`StubRuntime`] instance to add the given stub response. /// /// Panics if the stub response cannot be encoded using Candid. pub fn add_stub_response(self, stub_response: Out) -> Self { @@ -67,7 +67,7 @@ impl StubRuntime { self } - /// Mutate the [`ic_canister_runtime::StubRuntime`] instance to add the given stub error. + /// Mutate the [`StubRuntime`] instance to add the given stub error. pub fn add_stub_error(self, stub_error: impl Into) -> Self { self.call_results .try_lock() From 5b1c7f90ecf41f433fbdc91a392264a6e9250b40 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 17 Feb 2026 13:16:12 +0100 Subject: [PATCH 6/6] Rename test variable --- ic-canister-runtime/src/stub/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ic-canister-runtime/src/stub/tests.rs b/ic-canister-runtime/src/stub/tests.rs index 96a0021..e2de1f1 100644 --- a/ic-canister-runtime/src/stub/tests.rs +++ b/ic-canister-runtime/src/stub/tests.rs @@ -81,10 +81,10 @@ async fn should_return_multiple_stub_responses() { .query_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS) .await; assert_eq!(result3, Ok(expected3)); - let result3: Result = runtime + let result4: Result = runtime .update_call(DEFAULT_PRINCIPAL, DEFAULT_METHOD, DEFAULT_ARGS, 0) .await; - assert_eq!(result3, Err(expected4)); + assert_eq!(result4, Err(expected4)); } #[tokio::test]