1use candid::{CandidType, Principal, decode_one, encode_args, encode_one, utils::ArgumentEncoder};
2use canic::{
3 Error,
4 cdk::types::TC,
5 dto::{
6 abi::v1::CanisterInitPayload,
7 env::EnvBootstrapArgs,
8 subnet::SubnetIdentity,
9 topology::{AppDirectoryArgs, SubnetDirectoryArgs, SubnetRegistryResponse},
10 },
11 ids::CanisterRole,
12 protocol,
13};
14use pocket_ic::{PocketIc, PocketIcBuilder};
15use serde::de::DeserializeOwned;
16use std::{
17 collections::HashMap,
18 ops::{Deref, DerefMut},
19 panic::{AssertUnwindSafe, catch_unwind},
20 time::Duration,
21};
22
23mod baseline;
24mod process_lock;
25mod standalone;
26mod startup;
27
28pub use baseline::{
29 CachedPicBaseline, CachedPicBaselineGuard, ControllerSnapshots, acquire_cached_pic_baseline,
30};
31pub use process_lock::{
32 PicSerialGuard, PicSerialGuardError, acquire_pic_serial_guard, try_acquire_pic_serial_guard,
33};
34pub use startup::PicStartError;
35const INSTALL_CYCLES: u128 = 500 * TC;
36
37#[derive(Debug, Eq, PartialEq)]
42pub struct PicInstallError {
43 canister_id: Principal,
44 message: String,
45}
46
47#[derive(Debug)]
52pub enum StandaloneCanisterFixtureError {
53 SerialGuard(PicSerialGuardError),
54 Start(PicStartError),
55 Install(PicInstallError),
56}
57
58pub use standalone::{
59 StandaloneCanisterFixture, install_prebuilt_canister, install_prebuilt_canister_with_cycles,
60 install_standalone_canister, try_install_prebuilt_canister,
61 try_install_prebuilt_canister_with_cycles,
62};
63
64#[must_use]
73pub fn pic() -> Pic {
74 try_pic().unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
75}
76
77pub fn try_pic() -> Result<Pic, PicStartError> {
79 PicBuilder::new().with_application_subnet().try_build()
80}
81
82pub fn wait_until_ready(pic: &PocketIc, canister_id: Principal, tick_limit: usize) {
84 let payload = encode_args(()).expect("encode empty args");
85
86 for _ in 0..tick_limit {
87 if let Ok(bytes) = pic.query_call(
88 canister_id,
89 Principal::anonymous(),
90 protocol::CANIC_READY,
91 payload.clone(),
92 ) && let Ok(ready) = decode_one::<bool>(&bytes)
93 && ready
94 {
95 return;
96 }
97 pic.tick();
98 }
99
100 panic!("canister did not report ready in time: {canister_id}");
101}
102
103#[must_use]
105pub fn role_pid(
106 pic: &PocketIc,
107 root_id: Principal,
108 role: &'static str,
109 tick_limit: usize,
110) -> Principal {
111 for _ in 0..tick_limit {
112 let registry: Result<Result<SubnetRegistryResponse, Error>, Error> = {
113 let payload = encode_args(()).expect("encode empty args");
114 pic.query_call(
115 root_id,
116 Principal::anonymous(),
117 protocol::CANIC_SUBNET_REGISTRY,
118 payload,
119 )
120 .map_err(|err| {
121 Error::internal(format!(
122 "pocket_ic query_call failed (canister={root_id}, method={}): {err}",
123 protocol::CANIC_SUBNET_REGISTRY
124 ))
125 })
126 .and_then(|bytes| {
127 decode_one(&bytes).map_err(|err| {
128 Error::internal(format!("decode_one failed for subnet registry: {err}"))
129 })
130 })
131 };
132
133 if let Ok(Ok(registry)) = registry
134 && let Some(pid) = registry
135 .0
136 .into_iter()
137 .find(|entry| entry.role == CanisterRole::new(role))
138 .map(|entry| entry.pid)
139 {
140 return pid;
141 }
142
143 pic.tick();
144 }
145
146 panic!("{role} canister must be registered");
147}
148
149pub struct PicBuilder(PocketIcBuilder);
160
161#[expect(clippy::new_without_default)]
162impl PicBuilder {
163 #[must_use]
165 pub fn new() -> Self {
166 Self(PocketIcBuilder::new())
167 }
168
169 #[must_use]
171 pub fn with_application_subnet(mut self) -> Self {
172 self.0 = self.0.with_application_subnet();
173 self
174 }
175
176 #[must_use]
178 pub fn with_ii_subnet(mut self) -> Self {
179 self.0 = self.0.with_ii_subnet();
180 self
181 }
182
183 #[must_use]
185 pub fn with_nns_subnet(mut self) -> Self {
186 self.0 = self.0.with_nns_subnet();
187 self
188 }
189
190 #[must_use]
192 pub fn build(self) -> Pic {
193 self.try_build()
194 .unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
195 }
196
197 pub fn try_build(self) -> Result<Pic, PicStartError> {
199 startup::try_build_pic(AssertUnwindSafe(self.0).0)
200 }
201}
202
203impl PicInstallError {
204 #[must_use]
206 pub const fn new(canister_id: Principal, message: String) -> Self {
207 Self {
208 canister_id,
209 message,
210 }
211 }
212
213 #[must_use]
215 pub const fn canister_id(&self) -> Principal {
216 self.canister_id
217 }
218
219 #[must_use]
221 pub fn message(&self) -> &str {
222 &self.message
223 }
224}
225
226impl std::fmt::Display for PicInstallError {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(
229 f,
230 "failed to install canister {}: {}",
231 self.canister_id, self.message
232 )
233 }
234}
235
236impl std::error::Error for PicInstallError {}
237
238impl std::fmt::Display for StandaloneCanisterFixtureError {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 match self {
241 Self::SerialGuard(err) => write!(f, "{err}"),
242 Self::Start(err) => write!(f, "{err}"),
243 Self::Install(err) => write!(f, "{err}"),
244 }
245 }
246}
247
248impl std::error::Error for StandaloneCanisterFixtureError {
249 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
250 match self {
251 Self::SerialGuard(err) => Some(err),
252 Self::Start(err) => Some(err),
253 Self::Install(err) => Some(err),
254 }
255 }
256}
257
258pub struct Pic {
268 inner: PocketIc,
269}
270
271impl Pic {
272 #[must_use]
274 pub fn current_time_nanos(&self) -> u64 {
275 self.inner.get_time().as_nanos_since_unix_epoch()
276 }
277
278 pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
280 let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
281 self.inner.set_time(restored);
282 self.inner.set_certified_time(restored);
283 }
284
285 pub fn create_and_install_root_canister(&self, wasm: Vec<u8>) -> Result<Principal, Error> {
287 let init_bytes = install_root_args()?;
288
289 Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
290 }
291
292 pub fn create_and_install_canister(
296 &self,
297 role: CanisterRole,
298 wasm: Vec<u8>,
299 ) -> Result<Principal, Error> {
300 let init_bytes = install_args(role)?;
301
302 Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
303 }
304
305 #[must_use]
310 pub fn create_and_install_with_args(
311 &self,
312 wasm: Vec<u8>,
313 init_bytes: Vec<u8>,
314 install_cycles: u128,
315 ) -> Principal {
316 self.try_create_and_install_with_args(wasm, init_bytes, install_cycles)
317 .unwrap_or_else(|err| panic!("{err}"))
318 }
319
320 pub fn try_create_and_install_with_args(
322 &self,
323 wasm: Vec<u8>,
324 init_bytes: Vec<u8>,
325 install_cycles: u128,
326 ) -> Result<Principal, PicInstallError> {
327 self.try_create_funded_and_install(wasm, init_bytes, install_cycles)
328 }
329
330 pub fn wait_for_ready(&self, canister_id: Principal, tick_limit: usize, context: &str) {
332 for _ in 0..tick_limit {
333 self.tick();
334 if self.fetch_ready(canister_id) {
335 return;
336 }
337 }
338
339 self.dump_canister_debug(canister_id, context);
340 panic!("{context}: canister {canister_id} did not become ready after {tick_limit} ticks");
341 }
342
343 pub fn wait_for_all_ready<I>(&self, canister_ids: I, tick_limit: usize, context: &str)
345 where
346 I: IntoIterator<Item = Principal>,
347 {
348 let canister_ids = canister_ids.into_iter().collect::<Vec<_>>();
349
350 for _ in 0..tick_limit {
351 self.tick();
352 if canister_ids
353 .iter()
354 .copied()
355 .all(|canister_id| self.fetch_ready(canister_id))
356 {
357 return;
358 }
359 }
360
361 for canister_id in &canister_ids {
362 self.dump_canister_debug(*canister_id, context);
363 }
364 panic!("{context}: canisters did not become ready after {tick_limit} ticks");
365 }
366
367 pub fn wait_out_install_code_rate_limit(&self, cooldown: Duration) {
369 self.advance_time(cooldown);
370 self.tick_n(2);
371 }
372
373 pub fn retry_install_code_ok<T, F>(
375 &self,
376 retry_limit: usize,
377 cooldown: Duration,
378 mut op: F,
379 ) -> Result<T, String>
380 where
381 F: FnMut() -> Result<T, String>,
382 {
383 let mut last_err = None;
384
385 for _ in 0..retry_limit {
386 match op() {
387 Ok(value) => return Ok(value),
388 Err(err) if is_install_code_rate_limited(&err) => {
389 last_err = Some(err);
390 self.wait_out_install_code_rate_limit(cooldown);
391 }
392 Err(err) => return Err(err),
393 }
394 }
395
396 Err(last_err.unwrap_or_else(|| "install_code retry loop exhausted".to_string()))
397 }
398
399 pub fn retry_install_code_err<F>(
401 &self,
402 retry_limit: usize,
403 cooldown: Duration,
404 first: Result<(), String>,
405 mut op: F,
406 ) -> Result<(), String>
407 where
408 F: FnMut() -> Result<(), String>,
409 {
410 match first {
411 Ok(()) => return Ok(()),
412 Err(err) if !is_install_code_rate_limited(&err) => return Err(err),
413 Err(_) => {}
414 }
415
416 self.wait_out_install_code_rate_limit(cooldown);
417
418 for _ in 1..retry_limit {
419 match op() {
420 Ok(()) => return Ok(()),
421 Err(err) if is_install_code_rate_limited(&err) => {
422 self.wait_out_install_code_rate_limit(cooldown);
423 }
424 Err(err) => return Err(err),
425 }
426 }
427
428 op()
429 }
430
431 pub fn dump_canister_debug(&self, canister_id: Principal, context: &str) {
433 eprintln!("{context}: debug for canister {canister_id}");
434
435 match self.canister_status(canister_id, None) {
436 Ok(status) => eprintln!("canister_status: {status:?}"),
437 Err(err) => eprintln!("canister_status failed: {err:?}"),
438 }
439
440 match self.fetch_canister_logs(canister_id, Principal::anonymous()) {
441 Ok(records) => {
442 if records.is_empty() {
443 eprintln!("canister logs: <empty>");
444 } else {
445 for record in records {
446 eprintln!("canister log: {record:?}");
447 }
448 }
449 }
450 Err(err) => eprintln!("fetch_canister_logs failed: {err:?}"),
451 }
452 }
453
454 pub fn capture_controller_snapshots<I>(
456 &self,
457 controller_id: Principal,
458 canister_ids: I,
459 ) -> Option<ControllerSnapshots>
460 where
461 I: IntoIterator<Item = Principal>,
462 {
463 let mut snapshots = HashMap::new();
464
465 for canister_id in canister_ids {
466 let Some(snapshot) = self.try_take_controller_snapshot(controller_id, canister_id)
467 else {
468 eprintln!(
469 "capture_controller_snapshots: snapshot capture unavailable for {canister_id}"
470 );
471 return None;
472 };
473 snapshots.insert(canister_id, snapshot);
474 }
475
476 Some(ControllerSnapshots::new(snapshots))
477 }
478
479 pub fn restore_controller_snapshots(
481 &self,
482 controller_id: Principal,
483 snapshots: &ControllerSnapshots,
484 ) {
485 for (canister_id, snapshot_id, sender) in snapshots.iter() {
486 self.restore_controller_snapshot(controller_id, canister_id, sender, snapshot_id);
487 }
488 }
489
490 pub fn update_call<T, A>(
492 &self,
493 canister_id: Principal,
494 method: &str,
495 args: A,
496 ) -> Result<T, Error>
497 where
498 T: CandidType + DeserializeOwned,
499 A: ArgumentEncoder,
500 {
501 let bytes: Vec<u8> = encode_args(args)
502 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
503 let result = self
504 .inner
505 .update_call(canister_id, Principal::anonymous(), method, bytes)
506 .map_err(|err| {
507 Error::internal(format!(
508 "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
509 ))
510 })?;
511
512 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
513 }
514
515 pub fn update_call_as<T, A>(
517 &self,
518 canister_id: Principal,
519 caller: Principal,
520 method: &str,
521 args: A,
522 ) -> Result<T, Error>
523 where
524 T: CandidType + DeserializeOwned,
525 A: ArgumentEncoder,
526 {
527 let bytes: Vec<u8> = encode_args(args)
528 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
529 let result = self
530 .inner
531 .update_call(canister_id, caller, method, bytes)
532 .map_err(|err| {
533 Error::internal(format!(
534 "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
535 ))
536 })?;
537
538 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
539 }
540
541 pub fn query_call<T, A>(
543 &self,
544 canister_id: Principal,
545 method: &str,
546 args: A,
547 ) -> Result<T, Error>
548 where
549 T: CandidType + DeserializeOwned,
550 A: ArgumentEncoder,
551 {
552 let bytes: Vec<u8> = encode_args(args)
553 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
554 let result = self
555 .inner
556 .query_call(canister_id, Principal::anonymous(), method, bytes)
557 .map_err(|err| {
558 Error::internal(format!(
559 "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
560 ))
561 })?;
562
563 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
564 }
565
566 pub fn query_call_as<T, A>(
568 &self,
569 canister_id: Principal,
570 caller: Principal,
571 method: &str,
572 args: A,
573 ) -> Result<T, Error>
574 where
575 T: CandidType + DeserializeOwned,
576 A: ArgumentEncoder,
577 {
578 let bytes: Vec<u8> = encode_args(args)
579 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
580 let result = self
581 .inner
582 .query_call(canister_id, caller, method, bytes)
583 .map_err(|err| {
584 Error::internal(format!(
585 "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
586 ))
587 })?;
588
589 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
590 }
591
592 pub fn tick_n(&self, times: usize) {
594 for _ in 0..times {
595 self.tick();
596 }
597 }
598
599 fn try_create_funded_and_install(
601 &self,
602 wasm: Vec<u8>,
603 init_bytes: Vec<u8>,
604 install_cycles: u128,
605 ) -> Result<Principal, PicInstallError> {
606 let canister_id = self.create_canister();
607 self.add_cycles(canister_id, install_cycles);
608
609 let install = catch_unwind(AssertUnwindSafe(|| {
610 self.inner
611 .install_canister(canister_id, wasm, init_bytes, None);
612 }));
613 if let Err(payload) = install {
614 eprintln!("install_canister trapped for {canister_id}");
615 if let Ok(status) = self.inner.canister_status(canister_id, None) {
616 eprintln!("canister_status for {canister_id}: {status:?}");
617 }
618 if let Ok(logs) = self
619 .inner
620 .fetch_canister_logs(canister_id, Principal::anonymous())
621 {
622 for record in logs {
623 eprintln!("canister_log {canister_id}: {record:?}");
624 }
625 }
626 return Err(PicInstallError::new(
627 canister_id,
628 startup::panic_payload_to_string(payload.as_ref()),
629 ));
630 }
631
632 Ok(canister_id)
633 }
634
635 fn fetch_ready(&self, canister_id: Principal) -> bool {
637 match self.query_call(canister_id, protocol::CANIC_READY, ()) {
638 Ok(ready) => ready,
639 Err(err) => {
640 self.dump_canister_debug(canister_id, "query canic_ready failed");
641 panic!("query canic_ready failed: {err:?}");
642 }
643 }
644 }
645
646 fn try_take_controller_snapshot(
648 &self,
649 controller_id: Principal,
650 canister_id: Principal,
651 ) -> Option<(Vec<u8>, Option<Principal>)> {
652 let candidates = controller_sender_candidates(controller_id, canister_id);
653 let mut last_err = None;
654
655 for sender in candidates {
656 match self.take_canister_snapshot(canister_id, sender, None) {
657 Ok(snapshot) => return Some((snapshot.id, sender)),
658 Err(err) => last_err = Some((sender, err)),
659 }
660 }
661
662 if let Some((sender, err)) = last_err {
663 eprintln!(
664 "failed to capture canister snapshot for {canister_id} using sender {sender:?}: {err}"
665 );
666 }
667 None
668 }
669
670 fn restore_controller_snapshot(
672 &self,
673 controller_id: Principal,
674 canister_id: Principal,
675 snapshot_sender: Option<Principal>,
676 snapshot_id: &[u8],
677 ) {
678 let fallback_sender = if snapshot_sender.is_some() {
679 None
680 } else {
681 Some(controller_id)
682 };
683 let candidates = [snapshot_sender, fallback_sender];
684 let mut last_err = None;
685
686 for sender in candidates {
687 match self.load_canister_snapshot(canister_id, sender, snapshot_id.to_vec()) {
688 Ok(()) => return,
689 Err(err) => last_err = Some((sender, err)),
690 }
691 }
692
693 let (sender, err) =
694 last_err.expect("snapshot restore must have at least one sender attempt");
695 panic!(
696 "failed to restore canister snapshot for {canister_id} using sender {sender:?}: {err}"
697 );
698 }
699}
700
701fn is_install_code_rate_limited(message: &str) -> bool {
702 message.contains("CanisterInstallCodeRateLimited")
703}
704
705impl Deref for Pic {
706 type Target = PocketIc;
707
708 fn deref(&self) -> &Self::Target {
709 &self.inner
710 }
711}
712
713impl DerefMut for Pic {
714 fn deref_mut(&mut self) -> &mut Self::Target {
715 &mut self.inner
716 }
717}
718
719fn install_args(role: CanisterRole) -> Result<Vec<u8>, Error> {
734 if role.is_root() {
735 install_root_args()
736 } else {
737 let env = EnvBootstrapArgs {
740 prime_root_pid: None,
741 subnet_role: None,
742 subnet_pid: None,
743 root_pid: None,
744 canister_role: Some(role),
745 parent_pid: None,
746 };
747
748 let payload = CanisterInitPayload {
751 env,
752 app_directory: AppDirectoryArgs(Vec::new()),
753 subnet_directory: SubnetDirectoryArgs(Vec::new()),
754 };
755
756 encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
757 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))
758 }
759}
760
761fn install_root_args() -> Result<Vec<u8>, Error> {
762 encode_one(SubnetIdentity::Manual)
763 .map_err(|err| Error::internal(format!("encode_one failed: {err}")))
764}
765
766fn controller_sender_candidates(
768 controller_id: Principal,
769 canister_id: Principal,
770) -> [Option<Principal>; 2] {
771 if canister_id == controller_id {
772 [None, Some(controller_id)]
773 } else {
774 [Some(controller_id), None]
775 }
776}