use canic::{
cdk::types::Principal,
dto::{
canister::CanisterInfo,
env::EnvSnapshotResponse,
page::{Page, PageRequest},
state::{AppStateResponse, SubnetStateResponse},
topology::DirectoryEntryResponse,
},
ids::{CanisterRole, SubnetRole},
protocol,
};
use canic_testkit::pic::Pic;
use std::collections::HashMap;
pub fn assert_registry_parents(
pic: &Pic,
root_id: Principal,
expected: &[(CanisterRole, Option<Principal>)],
) {
let registry: Result<canic::dto::topology::SubnetRegistryResponse, canic::Error> = pic
.query_call(root_id, protocol::CANIC_SUBNET_REGISTRY, ())
.expect("query registry transport");
let registry = registry.expect("query registry application").0;
for (role, expected_parent) in expected {
let entry = registry
.iter()
.find(|entry| &entry.role == role)
.unwrap_or_else(|| panic!("missing {role} entry in registry"));
assert_eq!(
entry.record.parent_pid, *expected_parent,
"unexpected parent for {role}"
);
}
}
pub fn registry_pid_for_role(pic: &Pic, root_id: Principal, role: &CanisterRole) -> Principal {
let registry: Result<canic::dto::topology::SubnetRegistryResponse, canic::Error> = pic
.query_call(root_id, protocol::CANIC_SUBNET_REGISTRY, ())
.expect("query registry transport");
let registry = registry.expect("query registry application").0;
registry
.iter()
.find(|entry| &entry.role == role)
.map_or_else(
|| panic!("missing {role} entry in registry"),
|entry| entry.pid,
)
}
pub fn assert_child_env(pic: &Pic, child_pid: Principal, role: CanisterRole, root_id: Principal) {
let env: Result<EnvSnapshotResponse, canic::Error> = pic
.query_call(child_pid, protocol::CANIC_ENV, ())
.expect("query env transport");
let env = env.expect("query env application");
assert_eq!(
env.canister_role,
Some(role.clone()),
"env canister role for {role}"
);
assert_eq!(env.parent_pid, Some(root_id), "env parent for {role}");
assert_eq!(env.root_pid, Some(root_id), "env root for {role}");
assert_eq!(
env.prime_root_pid,
Some(root_id),
"env prime root for {role}"
);
assert_eq!(
env.subnet_role,
Some(SubnetRole::PRIME),
"env subnet role for {role}"
);
assert!(
env.subnet_pid.is_some(),
"env subnet pid should be set for {role}"
);
}
pub fn assert_directories_consistent(
pic: &Pic,
root_id: Principal,
subnet_directory: &HashMap<CanisterRole, Principal>,
) {
let root_app_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
.query_call(
root_id,
protocol::CANIC_APP_DIRECTORY,
(PageRequest {
limit: 100,
offset: 0,
},),
)
.expect("root app directory transport");
let root_app_dir = root_app_dir.expect("root app directory application");
let root_subnet_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
.query_call(
root_id,
protocol::CANIC_SUBNET_DIRECTORY,
(PageRequest {
limit: 100,
offset: 0,
},),
)
.expect("root subnet directory transport");
let root_subnet_dir = root_subnet_dir.expect("root subnet directory application");
for (role, pid) in subnet_directory.iter().filter(|(r, _)| !r.is_root()) {
let app_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
.query_call(
*pid,
protocol::CANIC_APP_DIRECTORY,
(PageRequest {
limit: 100,
offset: 0,
},),
)
.expect("child app directory transport");
let app_dir = app_dir.expect("child app directory application");
let subnet_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
.query_call(
*pid,
protocol::CANIC_SUBNET_DIRECTORY,
(PageRequest {
limit: 100,
offset: 0,
},),
)
.expect("child subnet directory transport");
let subnet_dir = subnet_dir.expect("child subnet directory application");
assert_eq!(
app_dir.entries,
root_app_dir.entries,
"app directory mismatch for {role} (child={}, root={})",
app_dir.entries.len(),
root_app_dir.entries.len(),
);
assert_eq!(
subnet_dir.entries,
root_subnet_dir.entries,
"subnet directory mismatch for {role} (child={}, root={})",
subnet_dir.entries.len(),
root_subnet_dir.entries.len(),
);
}
}
pub fn assert_children_match_registry(pic: &Pic, root_id: Principal) {
let registry: Result<canic::dto::topology::SubnetRegistryResponse, canic::Error> = pic
.query_call(root_id, protocol::CANIC_SUBNET_REGISTRY, ())
.expect("query registry transport");
let registry = registry.expect("query registry application").0;
let mut expected: Vec<CanisterInfo> = registry
.iter()
.filter(|entry| entry.record.parent_pid == Some(root_id))
.map(|entry| CanisterInfo {
pid: entry.pid,
role: entry.role.clone(),
parent_pid: entry.record.parent_pid,
module_hash: None, created_at: 0, })
.collect();
assert!(
!expected.is_empty(),
"registry should contain root children"
);
let page: Result<Page<CanisterInfo>, canic::Error> = pic
.query_call(
root_id,
protocol::CANIC_CANISTER_CHILDREN,
(PageRequest {
limit: 100,
offset: 0,
},),
)
.expect("query canister children transport");
let mut page = page.expect("query canister children application");
for entry in &mut page.entries {
entry.module_hash = None;
entry.created_at = 0;
}
expected.sort_by(|a, b| a.role.cmp(&b.role));
page.entries.sort_by(|a, b| a.role.cmp(&b.role));
assert_eq!(page.total, expected.len() as u64, "reported total mismatch");
assert_eq!(
page.entries, expected,
"child list from endpoint must match registry"
);
}
pub fn assert_state_endpoints_are_root_only(pic: &Pic, root_id: Principal, child_pid: Principal) {
let app_state: Result<AppStateResponse, canic::Error> = pic
.query_call(root_id, protocol::CANIC_APP_STATE, ())
.expect("root app state transport");
app_state.expect("root app state application");
let subnet_state: Result<SubnetStateResponse, canic::Error> = pic
.query_call(root_id, protocol::CANIC_SUBNET_STATE, ())
.expect("root subnet state transport");
subnet_state.expect("root subnet state application");
let child_app_state: Result<Result<AppStateResponse, canic::Error>, canic::Error> =
pic.query_call(child_pid, protocol::CANIC_APP_STATE, ());
let Err(err) = child_app_state else {
panic!("child app state endpoint should be absent")
};
assert_missing_method(&err, protocol::CANIC_APP_STATE);
let child_subnet_state: Result<Result<SubnetStateResponse, canic::Error>, canic::Error> =
pic.query_call(child_pid, protocol::CANIC_SUBNET_STATE, ());
let Err(err) = child_subnet_state else {
panic!("child subnet state endpoint should be absent")
};
assert_missing_method(&err, protocol::CANIC_SUBNET_STATE);
}
fn assert_missing_method(err: &canic::Error, method: &str) {
let message = err.message.as_str();
assert!(
message.contains(method),
"missing-method error should mention {method}: {message}"
);
assert!(
message.contains("not found")
|| message.contains("has no method")
|| message.contains("has no query method")
|| message.contains("has no update method")
|| message.contains("unknown method")
|| message.contains("did not find method"),
"expected missing-method transport failure for {method}, got: {message}"
);
}