1mod root;
2
3use candid::{CandidType, Principal, decode_one, encode_args, encode_one, utils::ArgumentEncoder};
4use canic::{
5 Error,
6 cdk::types::TC,
7 dto::{
8 abi::v1::CanisterInitPayload,
9 env::EnvBootstrapArgs,
10 subnet::SubnetIdentity,
11 topology::{AppDirectoryArgs, SubnetDirectoryArgs},
12 },
13 ids::CanisterRole,
14 protocol,
15};
16use pocket_ic::{PocketIc, PocketIcBuilder};
17use serde::de::DeserializeOwned;
18use std::{
19 collections::HashMap,
20 env, fs, io,
21 ops::{Deref, DerefMut},
22 panic::{AssertUnwindSafe, catch_unwind},
23 path::{Path, PathBuf},
24 process,
25 sync::{Mutex, MutexGuard},
26 thread,
27 time::{Duration, Instant},
28};
29
30pub use root::{
31 RootBaselineMetadata, RootBaselineSpec, build_root_cached_baseline,
32 ensure_root_release_artifacts_built, load_root_wasm, restore_root_cached_baseline,
33 setup_root_topology,
34};
35
36const INSTALL_CYCLES: u128 = 500 * TC;
37const PIC_PROCESS_LOCK_DIR_NAME: &str = "canic-pocket-ic.lock";
38const PIC_PROCESS_LOCK_RETRY_DELAY: Duration = Duration::from_millis(100);
39const PIC_PROCESS_LOCK_LOG_AFTER: Duration = Duration::from_secs(1);
40
41struct ControllerSnapshot {
42 snapshot_id: Vec<u8>,
43 sender: Option<Principal>,
44}
45
46struct ProcessLockGuard {
47 path: PathBuf,
48}
49
50pub struct ControllerSnapshots(HashMap<Principal, ControllerSnapshot>);
55
56pub struct CachedPicBaseline<T> {
61 pub pic: Pic,
62 pub snapshots: ControllerSnapshots,
63 pub metadata: T,
64}
65
66pub struct CachedPicBaselineGuard<'a, T> {
71 guard: MutexGuard<'a, Option<CachedPicBaseline<T>>>,
72}
73
74pub struct PicSerialGuard {
79 _process_lock: ProcessLockGuard,
80}
81
82#[must_use]
91pub fn pic() -> Pic {
92 PicBuilder::new().with_application_subnet().build()
93}
94
95#[must_use]
97pub fn acquire_pic_serial_guard() -> PicSerialGuard {
98 PicSerialGuard {
99 _process_lock: acquire_process_lock(),
100 }
101}
102
103pub fn acquire_cached_pic_baseline<T, F>(
105 slot: &'static Mutex<Option<CachedPicBaseline<T>>>,
106 build: F,
107) -> (CachedPicBaselineGuard<'static, T>, bool)
108where
109 F: FnOnce() -> CachedPicBaseline<T>,
110{
111 let mut guard = slot
112 .lock()
113 .unwrap_or_else(std::sync::PoisonError::into_inner);
114 let cache_hit = guard.is_some();
115
116 if !cache_hit {
117 *guard = Some(build());
118 }
119
120 (CachedPicBaselineGuard { guard }, cache_hit)
121}
122
123pub struct PicBuilder(PocketIcBuilder);
134
135#[expect(clippy::new_without_default)]
136impl PicBuilder {
137 #[must_use]
139 pub fn new() -> Self {
140 Self(PocketIcBuilder::new())
141 }
142
143 #[must_use]
145 pub fn with_application_subnet(mut self) -> Self {
146 self.0 = self.0.with_application_subnet();
147 self
148 }
149
150 #[must_use]
152 pub fn with_nns_subnet(mut self) -> Self {
153 self.0 = self.0.with_nns_subnet();
154 self
155 }
156
157 #[must_use]
159 pub fn build(self) -> Pic {
160 Pic {
161 inner: self.0.build(),
162 }
163 }
164}
165
166pub struct Pic {
176 inner: PocketIc,
177}
178
179impl<T> Deref for CachedPicBaselineGuard<'_, T> {
180 type Target = CachedPicBaseline<T>;
181
182 fn deref(&self) -> &Self::Target {
183 self.guard
184 .as_ref()
185 .expect("cached PocketIC baseline must exist")
186 }
187}
188
189impl<T> DerefMut for CachedPicBaselineGuard<'_, T> {
190 fn deref_mut(&mut self) -> &mut Self::Target {
191 self.guard
192 .as_mut()
193 .expect("cached PocketIC baseline must exist")
194 }
195}
196
197impl<T> CachedPicBaseline<T> {
198 pub fn capture<I>(
200 pic: Pic,
201 controller_id: Principal,
202 canister_ids: I,
203 metadata: T,
204 ) -> Option<Self>
205 where
206 I: IntoIterator<Item = Principal>,
207 {
208 let snapshots = pic.capture_controller_snapshots(controller_id, canister_ids)?;
209
210 Some(Self {
211 pic,
212 snapshots,
213 metadata,
214 })
215 }
216
217 pub fn restore(&self, controller_id: Principal) {
219 self.pic
220 .restore_controller_snapshots(controller_id, &self.snapshots);
221 }
222}
223
224impl Pic {
225 #[must_use]
227 pub fn current_time_nanos(&self) -> u64 {
228 self.inner.get_time().as_nanos_since_unix_epoch()
229 }
230
231 pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
233 let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
234 self.inner.set_time(restored);
235 self.inner.set_certified_time(restored);
236 }
237
238 pub fn create_and_install_root_canister(&self, wasm: Vec<u8>) -> Result<Principal, Error> {
240 let init_bytes = install_root_args()?;
241
242 Ok(self.create_funded_and_install(wasm, init_bytes))
243 }
244
245 pub fn create_and_install_canister(
249 &self,
250 role: CanisterRole,
251 wasm: Vec<u8>,
252 ) -> Result<Principal, Error> {
253 let init_bytes = install_args(role)?;
254
255 Ok(self.create_funded_and_install(wasm, init_bytes))
256 }
257
258 pub fn wait_for_ready(&self, canister_id: Principal, tick_limit: usize, context: &str) {
260 for _ in 0..tick_limit {
261 self.tick();
262 if self.fetch_ready(canister_id) {
263 return;
264 }
265 }
266
267 self.dump_canister_debug(canister_id, context);
268 panic!("{context}: canister {canister_id} did not become ready after {tick_limit} ticks");
269 }
270
271 pub fn wait_for_all_ready<I>(&self, canister_ids: I, tick_limit: usize, context: &str)
273 where
274 I: IntoIterator<Item = Principal>,
275 {
276 let canister_ids = canister_ids.into_iter().collect::<Vec<_>>();
277
278 for _ in 0..tick_limit {
279 self.tick();
280 if canister_ids
281 .iter()
282 .copied()
283 .all(|canister_id| self.fetch_ready(canister_id))
284 {
285 return;
286 }
287 }
288
289 for canister_id in &canister_ids {
290 self.dump_canister_debug(*canister_id, context);
291 }
292 panic!("{context}: canisters did not become ready after {tick_limit} ticks");
293 }
294
295 pub fn dump_canister_debug(&self, canister_id: Principal, context: &str) {
297 eprintln!("{context}: debug for canister {canister_id}");
298
299 match self.canister_status(canister_id, None) {
300 Ok(status) => eprintln!("canister_status: {status:?}"),
301 Err(err) => eprintln!("canister_status failed: {err:?}"),
302 }
303
304 match self.fetch_canister_logs(canister_id, Principal::anonymous()) {
305 Ok(records) => {
306 if records.is_empty() {
307 eprintln!("canister logs: <empty>");
308 } else {
309 for record in records {
310 eprintln!("canister log: {record:?}");
311 }
312 }
313 }
314 Err(err) => eprintln!("fetch_canister_logs failed: {err:?}"),
315 }
316 }
317
318 pub fn capture_controller_snapshots<I>(
320 &self,
321 controller_id: Principal,
322 canister_ids: I,
323 ) -> Option<ControllerSnapshots>
324 where
325 I: IntoIterator<Item = Principal>,
326 {
327 let mut snapshots = HashMap::new();
328
329 for canister_id in canister_ids {
330 let Some(snapshot) = self.try_take_controller_snapshot(controller_id, canister_id)
331 else {
332 eprintln!(
333 "capture_controller_snapshots: snapshot capture unavailable for {canister_id}"
334 );
335 return None;
336 };
337 snapshots.insert(canister_id, snapshot);
338 }
339
340 Some(ControllerSnapshots(snapshots))
341 }
342
343 pub fn restore_controller_snapshots(
345 &self,
346 controller_id: Principal,
347 snapshots: &ControllerSnapshots,
348 ) {
349 for (canister_id, snapshot) in &snapshots.0 {
350 self.restore_controller_snapshot(controller_id, *canister_id, snapshot);
351 }
352 }
353
354 pub fn update_call<T, A>(
356 &self,
357 canister_id: Principal,
358 method: &str,
359 args: A,
360 ) -> Result<T, Error>
361 where
362 T: CandidType + DeserializeOwned,
363 A: ArgumentEncoder,
364 {
365 let bytes: Vec<u8> = encode_args(args)
366 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
367 let result = self
368 .inner
369 .update_call(canister_id, Principal::anonymous(), method, bytes)
370 .map_err(|err| {
371 Error::internal(format!(
372 "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
373 ))
374 })?;
375
376 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
377 }
378
379 pub fn update_call_as<T, A>(
381 &self,
382 canister_id: Principal,
383 caller: Principal,
384 method: &str,
385 args: A,
386 ) -> Result<T, Error>
387 where
388 T: CandidType + DeserializeOwned,
389 A: ArgumentEncoder,
390 {
391 let bytes: Vec<u8> = encode_args(args)
392 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
393 let result = self
394 .inner
395 .update_call(canister_id, caller, method, bytes)
396 .map_err(|err| {
397 Error::internal(format!(
398 "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
399 ))
400 })?;
401
402 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
403 }
404
405 pub fn query_call<T, A>(
407 &self,
408 canister_id: Principal,
409 method: &str,
410 args: A,
411 ) -> Result<T, Error>
412 where
413 T: CandidType + DeserializeOwned,
414 A: ArgumentEncoder,
415 {
416 let bytes: Vec<u8> = encode_args(args)
417 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
418 let result = self
419 .inner
420 .query_call(canister_id, Principal::anonymous(), method, bytes)
421 .map_err(|err| {
422 Error::internal(format!(
423 "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
424 ))
425 })?;
426
427 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
428 }
429
430 pub fn query_call_as<T, A>(
432 &self,
433 canister_id: Principal,
434 caller: Principal,
435 method: &str,
436 args: A,
437 ) -> Result<T, Error>
438 where
439 T: CandidType + DeserializeOwned,
440 A: ArgumentEncoder,
441 {
442 let bytes: Vec<u8> = encode_args(args)
443 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
444 let result = self
445 .inner
446 .query_call(canister_id, caller, method, bytes)
447 .map_err(|err| {
448 Error::internal(format!(
449 "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
450 ))
451 })?;
452
453 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
454 }
455
456 pub fn tick_n(&self, times: usize) {
458 for _ in 0..times {
459 self.tick();
460 }
461 }
462
463 fn create_funded_and_install(&self, wasm: Vec<u8>, init_bytes: Vec<u8>) -> Principal {
465 let canister_id = self.create_canister();
466 self.add_cycles(canister_id, INSTALL_CYCLES);
467
468 let install = catch_unwind(AssertUnwindSafe(|| {
469 self.inner
470 .install_canister(canister_id, wasm, init_bytes, None);
471 }));
472 if let Err(err) = install {
473 eprintln!("install_canister trapped for {canister_id}");
474 if let Ok(status) = self.inner.canister_status(canister_id, None) {
475 eprintln!("canister_status for {canister_id}: {status:?}");
476 }
477 if let Ok(logs) = self
478 .inner
479 .fetch_canister_logs(canister_id, Principal::anonymous())
480 {
481 for record in logs {
482 eprintln!("canister_log {canister_id}: {record:?}");
483 }
484 }
485 std::panic::resume_unwind(err);
486 }
487
488 canister_id
489 }
490
491 fn fetch_ready(&self, canister_id: Principal) -> bool {
493 match self.query_call(canister_id, protocol::CANIC_READY, ()) {
494 Ok(ready) => ready,
495 Err(err) => {
496 self.dump_canister_debug(canister_id, "query canic_ready failed");
497 panic!("query canic_ready failed: {err:?}");
498 }
499 }
500 }
501
502 fn try_take_controller_snapshot(
504 &self,
505 controller_id: Principal,
506 canister_id: Principal,
507 ) -> Option<ControllerSnapshot> {
508 let candidates = controller_sender_candidates(controller_id, canister_id);
509 let mut last_err = None;
510
511 for sender in candidates {
512 match self.take_canister_snapshot(canister_id, sender, None) {
513 Ok(snapshot) => {
514 return Some(ControllerSnapshot {
515 snapshot_id: snapshot.id,
516 sender,
517 });
518 }
519 Err(err) => last_err = Some((sender, err)),
520 }
521 }
522
523 if let Some((sender, err)) = last_err {
524 eprintln!(
525 "failed to capture canister snapshot for {canister_id} using sender {sender:?}: {err}"
526 );
527 }
528 None
529 }
530
531 fn restore_controller_snapshot(
533 &self,
534 controller_id: Principal,
535 canister_id: Principal,
536 snapshot: &ControllerSnapshot,
537 ) {
538 let fallback_sender = if snapshot.sender.is_some() {
539 None
540 } else {
541 Some(controller_id)
542 };
543 let candidates = [snapshot.sender, fallback_sender];
544 let mut last_err = None;
545
546 for sender in candidates {
547 match self.load_canister_snapshot(canister_id, sender, snapshot.snapshot_id.clone()) {
548 Ok(()) => return,
549 Err(err) => last_err = Some((sender, err)),
550 }
551 }
552
553 let (sender, err) =
554 last_err.expect("snapshot restore must have at least one sender attempt");
555 panic!(
556 "failed to restore canister snapshot for {canister_id} using sender {sender:?}: {err}"
557 );
558 }
559}
560
561impl Drop for ProcessLockGuard {
562 fn drop(&mut self) {
563 let _ = fs::remove_file(process_lock_owner_path(&self.path));
564 let _ = fs::remove_dir(&self.path);
565 }
566}
567
568impl Deref for Pic {
569 type Target = PocketIc;
570
571 fn deref(&self) -> &Self::Target {
572 &self.inner
573 }
574}
575
576impl DerefMut for Pic {
577 fn deref_mut(&mut self) -> &mut Self::Target {
578 &mut self.inner
579 }
580}
581
582fn install_args(role: CanisterRole) -> Result<Vec<u8>, Error> {
597 if role.is_root() {
598 install_root_args()
599 } else {
600 let env = EnvBootstrapArgs {
603 prime_root_pid: None,
604 subnet_role: None,
605 subnet_pid: None,
606 root_pid: None,
607 canister_role: Some(role),
608 parent_pid: None,
609 };
610
611 let payload = CanisterInitPayload {
614 env,
615 app_directory: AppDirectoryArgs(Vec::new()),
616 subnet_directory: SubnetDirectoryArgs(Vec::new()),
617 };
618
619 encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
620 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))
621 }
622}
623
624fn install_root_args() -> Result<Vec<u8>, Error> {
625 encode_one(SubnetIdentity::Manual)
626 .map_err(|err| Error::internal(format!("encode_one failed: {err}")))
627}
628
629fn controller_sender_candidates(
631 controller_id: Principal,
632 canister_id: Principal,
633) -> [Option<Principal>; 2] {
634 if canister_id == controller_id {
635 [None, Some(controller_id)]
636 } else {
637 [Some(controller_id), None]
638 }
639}
640
641fn acquire_process_lock() -> ProcessLockGuard {
642 let lock_dir = env::temp_dir().join(PIC_PROCESS_LOCK_DIR_NAME);
643 let started_waiting = Instant::now();
644 let mut logged_wait = false;
645
646 loop {
647 match fs::create_dir(&lock_dir) {
648 Ok(()) => {
649 fs::write(
650 process_lock_owner_path(&lock_dir),
651 process::id().to_string(),
652 )
653 .unwrap_or_else(|err| {
654 let _ = fs::remove_dir(&lock_dir);
655 panic!(
656 "failed to record PocketIC process lock owner at {}: {err}",
657 lock_dir.display()
658 );
659 });
660
661 if logged_wait {
662 eprintln!(
663 "[canic_testkit::pic] acquired cross-process PocketIC lock at {}",
664 lock_dir.display()
665 );
666 }
667
668 return ProcessLockGuard { path: lock_dir };
669 }
670 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
671 if process_lock_is_stale(&lock_dir) {
672 let _ = fs::remove_file(process_lock_owner_path(&lock_dir));
673 let _ = fs::remove_dir(&lock_dir);
674 continue;
675 }
676
677 if !logged_wait && started_waiting.elapsed() >= PIC_PROCESS_LOCK_LOG_AFTER {
678 eprintln!(
679 "[canic_testkit::pic] waiting for cross-process PocketIC lock at {}",
680 lock_dir.display()
681 );
682 logged_wait = true;
683 }
684
685 thread::sleep(PIC_PROCESS_LOCK_RETRY_DELAY);
686 }
687 Err(err) => panic!(
688 "failed to create PocketIC process lock dir at {}: {err}",
689 lock_dir.display()
690 ),
691 }
692 }
693}
694
695fn process_lock_owner_path(lock_dir: &Path) -> PathBuf {
696 lock_dir.join("owner")
697}
698
699fn process_lock_is_stale(lock_dir: &Path) -> bool {
700 let Ok(pid_text) = fs::read_to_string(process_lock_owner_path(lock_dir)) else {
701 return true;
702 };
703 let Ok(pid) = pid_text.trim().parse::<u32>() else {
704 return true;
705 };
706
707 !Path::new("/proc").join(pid.to_string()).exists()
708}