From fdf055fa249f0588a7b5ccb4218d04276c936abb Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Mon, 17 Feb 2025 15:48:08 +0100 Subject: [PATCH] test(sns): Porting sns-testing to the ICP mono repo (#3979) This PR implements the next-gen [sns-testing](https://github.com/dfinity/sns-testing) tool. It can now be written in Rust and continuously tested against the latest SNS features. At the core of this tool lies the [PocketIC framework](https://crates.io/crates/pocket-ic/6.0.0), the testing framework capable of creating a fully deterministic environment for testing complex ICP dapp scenarios. This PR is authored by Serokell and reviewed by the DFINITY Foundation. Please refer to the reviews here: https://github.com/dfinity/ic/pull/3654 --------- Co-authored-by: Roman Melnikov Co-authored-by: Roman Melnikov --- Cargo.lock | 25 +++ Cargo.toml | 1 + packages/pocket-ic/CHANGELOG.md | 2 + packages/pocket-ic/src/lib.rs | 12 +- packages/pocket-ic/src/nonblocking.rs | 15 +- packages/pocket-ic/tests/tests.rs | 13 ++ rs/ledger_suite/icp/index/src/lib.rs | 2 +- rs/nervous_system/agent/src/pocketic_impl.rs | 6 +- .../src/pocket_ic_helpers.rs | 119 +++++++---- rs/nns/test_utils/BUILD.bazel | 1 + rs/nns/test_utils/Cargo.toml | 1 + rs/nns/test_utils/src/common.rs | 18 ++ rs/sns/testing/BUILD.bazel | 113 +++++++++++ rs/sns/testing/Cargo.toml | 39 ++++ rs/sns/testing/README.md | 35 ++++ rs/sns/testing/canister/canister.rs | 43 ++++ rs/sns/testing/canister/test.did | 3 + rs/sns/testing/src/lib.rs | 2 + rs/sns/testing/src/main.rs | 52 +++++ rs/sns/testing/src/nns_dapp.rs | 192 ++++++++++++++++++ rs/sns/testing/src/sns.rs | 118 +++++++++++ rs/sns/testing/tests/sns_testing_ci.rs | 64 ++++++ 22 files changed, 833 insertions(+), 43 deletions(-) create mode 100644 rs/sns/testing/BUILD.bazel create mode 100644 rs/sns/testing/Cargo.toml create mode 100644 rs/sns/testing/README.md create mode 100644 rs/sns/testing/canister/canister.rs create mode 100644 rs/sns/testing/canister/test.did create mode 100644 rs/sns/testing/src/lib.rs create mode 100644 rs/sns/testing/src/main.rs create mode 100644 rs/sns/testing/src/nns_dapp.rs create mode 100644 rs/sns/testing/src/sns.rs create mode 100644 rs/sns/testing/tests/sns_testing_ci.rs diff --git a/Cargo.lock b/Cargo.lock index e158a5101115..eb2843d62a18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10956,6 +10956,7 @@ dependencies = [ "ic-crypto-test-utils-ni-dkg", "ic-crypto-test-utils-reproducible-rng", "ic-crypto-utils-ni-dkg", + "ic-icp-index", "ic-icrc1", "ic-management-canister-types-private", "ic-nervous-system-clients", @@ -12600,6 +12601,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "ic-sns-testing" +version = "0.9.0" +dependencies = [ + "candid", + "canister-test", + "clap 4.5.29", + "futures", + "ic-base-types", + "ic-cdk 0.17.1", + "ic-management-canister-types-private", + "ic-nervous-system-agent", + "ic-nervous-system-integration-tests", + "ic-nns-common", + "ic-nns-constants", + "ic-nns-test-utils", + "ic-sns-governance-api", + "ic-sns-swap", + "pocket-ic", + "reqwest 0.12.12", + "serde", + "tokio", +] + [[package]] name = "ic-sns-wasm" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4d1b0b97846c..f1b5b9886dec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -346,6 +346,7 @@ members = [ "rs/sns/swap", "rs/sns/swap/proto_library", "rs/sns/test_utils", + "rs/sns/testing", "rs/starter", "rs/state_manager", "rs/state_machine_tests", diff --git a/packages/pocket-ic/CHANGELOG.md b/packages/pocket-ic/CHANGELOG.md index e426dc3ef758..5b9879c22100 100644 --- a/packages/pocket-ic/CHANGELOG.md +++ b/packages/pocket-ic/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- The function `PocketIc::try_get_controllers` which gets the controllers of a canister but doesn't panic if the target canister + doesn't exist. - The function `PocketIcBuilder::with_bitcoind_addrs` to specify multiple addresses and ports at which `bitcoind` processes are listening. - The function `PocketIc::query_call_with_effective_principal` for making generic query calls (including management canister query calls). - The function `PocketIc::ingress_status` to fetch the status of an update call submitted through an ingress message. diff --git a/packages/pocket-ic/src/lib.rs b/packages/pocket-ic/src/lib.rs index b520f8043452..f84553dc1abc 100644 --- a/packages/pocket-ic/src/lib.rs +++ b/packages/pocket-ic/src/lib.rs @@ -70,7 +70,7 @@ use candid::{ Principal, }; use ic_transport_types::SubnetMetrics; -use reqwest::Url; +use reqwest::{StatusCode, Url}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use slog::Level; @@ -649,6 +649,16 @@ impl PocketIc { runtime.block_on(async { self.pocket_ic.get_controllers(canister_id).await }) } + /// Get the controllers of a canister. + #[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))] + pub fn try_get_controllers( + &self, + canister_id: CanisterId, + ) -> Result, (StatusCode, String)> { + let runtime = self.runtime.clone(); + runtime.block_on(async { self.pocket_ic.try_get_controllers(canister_id).await }) + } + /// Get the current cycles balance of a canister. #[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))] pub fn cycle_balance(&self, canister_id: CanisterId) -> u128 { diff --git a/packages/pocket-ic/src/nonblocking.rs b/packages/pocket-ic/src/nonblocking.rs index e58a8b975364..84d04a1f141d 100644 --- a/packages/pocket-ic/src/nonblocking.rs +++ b/packages/pocket-ic/src/nonblocking.rs @@ -519,16 +519,25 @@ impl PocketIc { /// Panics if the canister does not exist. #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))] pub async fn get_controllers(&self, canister_id: CanisterId) -> Vec { + self.try_get_controllers(canister_id).await.unwrap() + } + + /// Get the controllers of a canister. + #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))] + pub async fn try_get_controllers( + &self, + canister_id: CanisterId, + ) -> Result, (StatusCode, String)> { let endpoint = "read/get_controllers"; - let result: Vec = self - .post( + let result: Result, (StatusCode, String)> = self + .try_post( endpoint, RawCanisterId { canister_id: canister_id.as_slice().to_vec(), }, ) .await; - result.into_iter().map(|p| p.into()).collect() + result.map(|v| v.into_iter().map(|p| p.into()).collect()) } /// Get the current cycles balance of a canister. diff --git a/packages/pocket-ic/tests/tests.rs b/packages/pocket-ic/tests/tests.rs index 5eb863b3c4bb..8e9874b52e9f 100644 --- a/packages/pocket-ic/tests/tests.rs +++ b/packages/pocket-ic/tests/tests.rs @@ -1739,6 +1739,19 @@ fn get_controllers_of_nonexisting_canister() { let _ = pic.get_controllers(canister_id); } +#[test] +fn try_get_controllers_of_nonexisting_canister() { + let pic = PocketIc::new(); + + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 100_000_000_000_000); + pic.stop_canister(canister_id, None).unwrap(); + pic.delete_canister(canister_id, None).unwrap(); + + let res = pic.try_get_controllers(canister_id); + assert!(res.is_err()) +} + #[test] fn test_canister_snapshots() { let pic = PocketIc::new(); diff --git a/rs/ledger_suite/icp/index/src/lib.rs b/rs/ledger_suite/icp/index/src/lib.rs index 76912ce92050..231ec789b5b6 100644 --- a/rs/ledger_suite/icp/index/src/lib.rs +++ b/rs/ledger_suite/icp/index/src/lib.rs @@ -6,7 +6,7 @@ use serde_bytes::ByteBuf; pub mod logs; -#[derive(Debug, CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize)] pub struct InitArg { pub ledger_id: Principal, } diff --git a/rs/nervous_system/agent/src/pocketic_impl.rs b/rs/nervous_system/agent/src/pocketic_impl.rs index 9190c8e3b901..64a22a042905 100644 --- a/rs/nervous_system/agent/src/pocketic_impl.rs +++ b/rs/nervous_system/agent/src/pocketic_impl.rs @@ -148,7 +148,11 @@ impl CallCanisters for PocketIcAgent<'_> { ) -> Result { let canister_id = canister_id.into(); - let controllers = self.pocket_ic.get_controllers(canister_id).await; + let controllers = self + .pocket_ic + .try_get_controllers(canister_id) + .await + .unwrap_or(vec![]); let Some(controller) = controllers.into_iter().last() else { return Err(Self::Error::BlackHole); diff --git a/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs b/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs index 0ac6fe09568c..d4288026ccf2 100644 --- a/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs +++ b/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs @@ -14,8 +14,8 @@ use ic_nervous_system_common_test_keys::{TEST_NEURON_1_ID, TEST_NEURON_1_OWNER_P use ic_nns_common::pb::v1::{NeuronId, ProposalId}; use ic_nns_constants::{ self, ALL_NNS_CANISTER_IDS, CYCLES_MINTING_CANISTER_ID, GOVERNANCE_CANISTER_ID, - LEDGER_CANISTER_ID, LIFELINE_CANISTER_ID, REGISTRY_CANISTER_ID, ROOT_CANISTER_ID, - SNS_WASM_CANISTER_ID, + LEDGER_CANISTER_ID, LEDGER_INDEX_CANISTER_ID, LIFELINE_CANISTER_ID, REGISTRY_CANISTER_ID, + ROOT_CANISTER_ID, SNS_WASM_CANISTER_ID, }; use ic_nns_governance_api::pb::v1::{ install_code::CanisterInstallMode, manage_neuron_response, CreateServiceNervousSystem, @@ -26,10 +26,11 @@ use ic_nns_governance_api::pb::v1::{ }; use ic_nns_test_utils::{ common::{ - build_cmc_wasm, build_governance_wasm, build_ledger_wasm, build_lifeline_wasm, - build_mainnet_cmc_wasm, build_mainnet_governance_wasm, build_mainnet_ledger_wasm, - build_mainnet_lifeline_wasm, build_mainnet_registry_wasm, build_mainnet_root_wasm, - build_mainnet_sns_wasms_wasm, build_registry_wasm, build_root_wasm, build_sns_wasms_wasm, + build_cmc_wasm, build_governance_wasm, build_index_wasm, build_ledger_wasm, + build_lifeline_wasm, build_mainnet_cmc_wasm, build_mainnet_governance_wasm, + build_mainnet_index_wasm, build_mainnet_ledger_wasm, build_mainnet_lifeline_wasm, + build_mainnet_registry_wasm, build_mainnet_root_wasm, build_mainnet_sns_wasms_wasm, + build_registry_wasm, build_root_wasm, build_sns_wasms_wasm, build_test_governance_wasm, NnsInitPayloadsBuilder, }, sns_wasm::{ @@ -181,10 +182,10 @@ pub async fn install_canister_with_controllers( .await .unwrap(); pocket_ic - .install_canister(canister_id, wasm.bytes(), arg, controller_principal) + .add_cycles(canister_id, STARTING_CYCLES_PER_CANISTER) .await; pocket_ic - .add_cycles(canister_id, STARTING_CYCLES_PER_CANISTER) + .install_canister(canister_id, wasm.bytes(), arg, controller_principal) .await; let subnet_id = pocket_ic.get_subnet(canister_id).await.unwrap(); println!( @@ -329,6 +330,8 @@ pub struct NnsInstaller { initial_balances: Vec<(AccountIdentifier, Tokens)>, with_cycles_minting_canister: bool, with_cycles_ledger: bool, + with_index_canister: bool, + with_test_governance_canister: bool, } impl NnsInstaller { @@ -389,6 +392,16 @@ impl NnsInstaller { self } + pub fn with_index_canister(&mut self) -> &mut Self { + self.with_index_canister = true; + self + } + + pub fn with_test_governance_canister(&mut self) -> &mut Self { + self.with_test_governance_canister = true; + self + } + /// Installs the NNS canister suite. /// /// Ensures that there is a whale neuron with `TEST_NEURON_1_ID`. @@ -401,6 +414,10 @@ impl NnsInstaller { .mainnet_nns_canister_versions .expect("Please explicitly request either mainnet or tip-of-the-branch NNS version."); + assert!(!(with_mainnet_canister_versions && self.with_test_governance_canister), + "The test version of the governance canister cannot be used with mainnet versions of the NNS canisters" + ); + let topology = pocket_ic.topology().await; let sns_subnet_id = topology.get_sns().expect("No SNS subnet found"); @@ -446,7 +463,11 @@ impl NnsInstaller { ) } else { ( - build_governance_wasm(), + if self.with_test_governance_canister { + build_test_governance_wasm() + } else { + build_governance_wasm() + }, build_ledger_wasm(), build_root_wasm(), build_lifeline_wasm(), @@ -547,6 +568,23 @@ impl NnsInstaller { cycles_ledger::install(pocket_ic).await; } + if self.with_index_canister { + let ledger_index_wasm = if with_mainnet_canister_versions { + build_mainnet_index_wasm() + } else { + build_index_wasm() + }; + install_canister( + pocket_ic, + "Index", + LEDGER_INDEX_CANISTER_ID, + Encode!(&nns_init_payload.index).unwrap(), + ledger_index_wasm, + Some(ROOT_CANISTER_ID.get()), + ) + .await; + } + nns_init_payload .governance .neurons @@ -690,11 +728,13 @@ pub mod cycles_ledger { /// Arguments /// 1. `with_mainnet_nns_canister_versions` is a flag indicating whether the mainnet /// (or, therwise, tip-of-this-branch) WASM versions should be installed. -/// 2. `initial_balances` is a `Vec` of `(test_user_icp_ledger_account, +/// 2. `with_test_nns_governance_canister` is a flag indicating whether the test version of +/// the governance canister should be installed. Mutually exclusive with `with_mainnet_nns_canister_versions`. +/// 3. `initial_balances` is a `Vec` of `(test_user_icp_ledger_account, /// test_user_icp_ledger_initial_balance)` pairs, representing some initial ICP balances. /// 3. `custom_registry_mutations` are custom mutations for the inital Registry. These -/// should mutations should comply with Registry invariants, otherwise this function will fail. -/// 4. `maturity_equivalent_icp_e8s` - hotkeys of the 1st NNS (Neurons' Fund-participating) neuron. +/// mutations should comply with Registry invariants, otherwise this function will fail. +/// 4. `neurons_fund_hotkeys` - hotkeys of the 1st NNS (Neurons' Fund-participating) neuron. /// /// Returns /// 1. A list of `controller_principal_id`s of pre-configured NNS neurons. @@ -1058,19 +1098,14 @@ where assert!(expected_event_interval_seconds.start < expected_event_interval_seconds.end, "expected_event_interval_seconds.start must be less than expected_event_interval_seconds.end"); let timeout_seconds = expected_event_interval_seconds.end - expected_event_interval_seconds.start; - pocket_ic - .advance_time(Duration::from_secs(expected_event_interval_seconds.start)) - .await; + progress_pocket_ic(pocket_ic, expected_event_interval_seconds.start).await; let mut counter = 0; let num_ticks = timeout_seconds.min(500); let seconds_per_tick = (timeout_seconds as f64 / num_ticks as f64).ceil() as u64; loop { - pocket_ic - .advance_time(Duration::from_secs(seconds_per_tick)) - .await; - pocket_ic.tick().await; + progress_pocket_ic(pocket_ic, seconds_per_tick).await; let observed = observe(pocket_ic).await; if observed == *expected { @@ -1086,6 +1121,17 @@ where } } +// Using 'advance_time' in live mode breaks certificate checking, so we have to wait +// for the time to pass naturally. +async fn progress_pocket_ic(pocket_ic: &PocketIc, seconds: u64) { + if pocket_ic.url().is_some() { + std::thread::sleep(Duration::from_secs(seconds)); + } else { + pocket_ic.tick().await; + pocket_ic.advance_time(Duration::from_secs(seconds)).await; + } +} + pub mod nns { use super::*; pub mod governance { @@ -1192,11 +1238,10 @@ pub mod nns { pocket_ic: &PocketIc, proposal_id: u64, ) -> Result { - // We create some blocks until the proposal has finished executing (`pocket_ic.tick()`). + // We progress the blockchain until the proposal has finished executing. let mut last_proposal_info = None; for _attempt_count in 1..=100 { - pocket_ic.tick().await; - pocket_ic.advance_time(Duration::from_secs(1)).await; + progress_pocket_ic(pocket_ic, 1).await; let proposal_info_result = nns_get_proposal_info(pocket_ic, proposal_id, PrincipalId::new_anonymous()) .await; @@ -1665,8 +1710,7 @@ pub mod sns { .await; for _ in 0..20 { - pocket_ic.advance_time(Duration::from_secs(10)).await; - pocket_ic.tick().await; + progress_pocket_ic(pocket_ic, 10).await; } let post_upgrade_version = sns.governance.version(pocket_ic).await; @@ -1836,11 +1880,10 @@ pub mod sns { canister_id: PrincipalId, proposal_id: sns_pb::ProposalId, ) -> Result { - // We create some blocks until the proposal has finished executing (`pocket_ic.tick()`). + // We progress the blockchain until the proposal has finished executing. let mut last_proposal_data = None; for _attempt_count in 1..=50 { - pocket_ic.tick().await; - pocket_ic.advance_time(Duration::from_secs(1)).await; + progress_pocket_ic(pocket_ic, 1).await; let proposal_result = get_proposal( pocket_ic, canister_id, @@ -2637,7 +2680,7 @@ pub mod sns { } } - // Helper function that calls tick on env until either the index canister has synced all + // Helper function that progress the blockchain until either the index canister has synced all // the blocks up to the last one in the ledger or enough attempts passed and therefore it fails. pub async fn wait_until_ledger_and_index_sync_is_completed( pocket_ic: &PocketIc, @@ -2648,8 +2691,7 @@ pub mod sns { let mut num_blocks_synced = u64::MAX; let mut chain_length = u64::MAX; for _i in 0..MAX_ATTEMPTS { - pocket_ic.tick().await; - pocket_ic.advance_time(Duration::from_secs(1)).await; + progress_pocket_ic(pocket_ic, 1).await; num_blocks_synced = index_ng::status(pocket_ic, index_canister_id) .await .num_blocks_synced @@ -2920,13 +2962,17 @@ pub mod sns { expected_lifecycle: Lifecycle, ) -> Result<(), String> { // The swap opens in up to 48 after the proposal for creating this SNS was executed. - pocket_ic - .advance_time(Duration::from_secs(48 * 60 * 60)) - .await; + // Waiting for 48 hours in live mode is not viable, but the live mode is supposed + // to use NNS governance canister with test feature to ensure that swap can be started + // immediately. + if pocket_ic.url().is_none() { + pocket_ic + .advance_time(Duration::from_secs(48 * 60 * 60)) + .await; + } let mut last_lifecycle = None; for _attempt_count in 1..=100 { - pocket_ic.tick().await; - pocket_ic.advance_time(Duration::from_secs(1)).await; + progress_pocket_ic(pocket_ic, 1).await; let response = get_lifecycle(pocket_ic, swap_canister_id).await; let lifecycle = Lifecycle::try_from(response.lifecycle.unwrap()).unwrap(); if lifecycle == expected_lifecycle { @@ -3090,8 +3136,7 @@ pub mod sns { ) -> Result { let mut last_auto_finalization_status = None; for _attempt_count in 1..=1000 { - pocket_ic.tick().await; - pocket_ic.advance_time(Duration::from_secs(1)).await; + progress_pocket_ic(pocket_ic, 1).await; let auto_finalization_status = get_auto_finalization_status(pocket_ic, swap_canister_id).await; match status { diff --git a/rs/nns/test_utils/BUILD.bazel b/rs/nns/test_utils/BUILD.bazel index 1af0fe1748d5..845996f9e428 100644 --- a/rs/nns/test_utils/BUILD.bazel +++ b/rs/nns/test_utils/BUILD.bazel @@ -15,6 +15,7 @@ BASE_DEPENDENCIES = [ "//rs/crypto/test_utils/reproducible_rng", "//rs/crypto/utils/ni_dkg", "//rs/ledger_suite/icp:icp_ledger", + "//rs/ledger_suite/icp/index:ic-icp-index", "//rs/ledger_suite/icrc1", "//rs/nervous_system/clients", "//rs/nervous_system/common", diff --git a/rs/nns/test_utils/Cargo.toml b/rs/nns/test_utils/Cargo.toml index 7339c806cfa8..c482eef19c66 100644 --- a/rs/nns/test_utils/Cargo.toml +++ b/rs/nns/test_utils/Cargo.toml @@ -28,6 +28,7 @@ ic-crypto-sha2 = { path = "../../crypto/sha2" } ic-crypto-test-utils-ni-dkg = { path = "../../crypto/test_utils/ni-dkg" } ic-crypto-test-utils-reproducible-rng = { path = "../../crypto/test_utils/reproducible_rng" } ic-crypto-utils-ni-dkg = { path = "../../crypto/utils/ni_dkg" } +ic-icp-index = { path = "../../ledger_suite/icp/index" } ic-icrc1 = { path = "../../ledger_suite/icrc1" } ic-management-canister-types-private = { path = "../../types/management_canister_types" } ic-nervous-system-clients = { path = "../../nervous_system/clients" } diff --git a/rs/nns/test_utils/src/common.rs b/rs/nns/test_utils/src/common.rs index a7fc7624f55e..926155ce7a1a 100644 --- a/rs/nns/test_utils/src/common.rs +++ b/rs/nns/test_utils/src/common.rs @@ -41,6 +41,7 @@ pub struct NnsInitPayloads { pub lifeline: LifelineCanisterInitPayload, pub genesis_token: Gtc, pub sns_wasms: SnsWasmCanisterInitPayload, + pub index: ic_icp_index::InitArg, } /// Builder to help create the initial payloads for the NNS canisters. @@ -53,6 +54,7 @@ pub struct NnsInitPayloadsBuilder { pub lifeline: LifelineCanisterInitPayloadBuilder, pub genesis_token: GenesisTokenCanisterInitPayloadBuilder, pub sns_wasms: SnsWasmCanisterInitPayloadBuilder, + pub index: ic_icp_index::InitArg, } #[allow(clippy::new_without_default)] @@ -94,6 +96,9 @@ impl NnsInitPayloadsBuilder { lifeline: LifelineCanisterInitPayloadBuilder::new(), genesis_token: GenesisTokenCanisterInitPayloadBuilder::new(), sns_wasms: SnsWasmCanisterInitPayloadBuilder::new(), + index: ic_icp_index::InitArg { + ledger_id: LEDGER_CANISTER_ID.get().into(), + }, } } @@ -283,6 +288,7 @@ impl NnsInitPayloadsBuilder { lifeline: self.lifeline.build(), genesis_token: self.genesis_token.build(), sns_wasms: self.sns_wasms.build(), + index: self.index.clone(), } } } @@ -372,6 +378,12 @@ pub fn build_sns_wasms_wasm() -> Wasm { Project::cargo_bin_maybe_from_env("sns-wasm-canister", &features) } +/// Build Wasm for Index canister for the ICP Ledger +pub fn build_index_wasm() -> Wasm { + let features = []; + Project::cargo_bin_maybe_from_env("ic-icp-index", &features) +} + /// Build mainnet Wasm for NNS SnsWasm canister pub fn build_mainnet_sns_wasms_wasm() -> Wasm { let features = []; @@ -394,3 +406,9 @@ pub fn build_mainnet_governance_wasm() -> Wasm { let features = []; Project::cargo_bin_maybe_from_env("mainnet-governance-canister", &features) } + +/// Build mainnet Wasm for Index canister for the ICP Ledger +pub fn build_mainnet_index_wasm() -> Wasm { + let features = []; + Project::cargo_bin_maybe_from_env("mainnet-ic-icp-index-canister", &features) +} diff --git a/rs/sns/testing/BUILD.bazel b/rs/sns/testing/BUILD.bazel new file mode 100644 index 000000000000..b63f3efa11c1 --- /dev/null +++ b/rs/sns/testing/BUILD.bazel @@ -0,0 +1,113 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("//bazel:canisters.bzl", "rust_canister") + +package(default_visibility = ["//visibility:public"]) + +DEPENDENCIES = [ + "//packages/pocket-ic", + "//rs/nervous_system/agent", + "//rs/nervous_system/integration_tests:nervous_system_integration_tests", + "//rs/nns/common", + "//rs/nns/test_utils", + "//rs/nns/constants", + "//rs/sns/governance/api", + "//rs/sns/swap:swap", + "//rs/rust_canisters/canister_test", + "//rs/types/base_types", + "//rs/types/management_canister_types", + "@crate_index//:candid", + "@crate_index//:clap", + "@crate_index//:futures", + "@crate_index//:reqwest", + "@crate_index//:tokio", +] + +DEV_DATA = [ + "//rs/ledger_suite/icp/archive:ledger-archive-node-canister-wasm", + "//rs/ledger_suite/icp/index:ic-icp-index-canister", + "//rs/ledger_suite/icp/ledger:ledger-canister-wasm", + "//rs/ledger_suite/icp/ledger:ledger-canister-wasm-notify-method", + "//rs/ledger_suite/icrc1/archive:archive_canister", + "//rs/ledger_suite/icrc1/index-ng:index_ng_canister", + "//rs/ledger_suite/icrc1/ledger:ledger_canister", + "//rs/nns/cmc:cycles-minting-canister", + "//rs/nns/governance:governance-canister", + "//rs/nns/governance:governance-canister-test", + "//rs/nns/handlers/root/impl:root-canister", + "//rs/nns/sns-wasm:sns-wasm-canister", + "//rs/pocket_ic_server:pocket-ic-server", + "//rs/registry/canister:registry-canister", + "//rs/sns/governance:sns-governance-canister", + "//rs/sns/root:sns-root-canister", + "//rs/sns/swap:sns-swap-canister", + "//rs/types/management_canister_types", + ":sns_testing_canister", + "@nns_dapp_canister//file", + "@sns_aggregator//file", + "@ii_dev_canister//file", +] + +DEV_ENV = { + "CYCLES_MINTING_CANISTER_WASM_PATH": "$(rootpath //rs/nns/cmc:cycles-minting-canister)", + "GOVERNANCE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/governance:governance-canister)", + "GOVERNANCE_CANISTER_TEST_WASM_PATH": "$(rootpath //rs/nns/governance:governance-canister-test)", + "REGISTRY_CANISTER_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", + "IC_ICRC1_ARCHIVE_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/archive:archive_canister)", + "IC_ICRC1_INDEX_NG_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/index-ng:index_ng_canister)", + "IC_ICRC1_LEDGER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/ledger:ledger_canister)", + "IC_ICP_INDEX_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/index:ic-icp-index-canister)", + "LEDGER_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/ledger:ledger-canister-wasm)", + "LEDGER_CANISTER_NOTIFY_METHOD_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/ledger:ledger-canister-wasm-notify-method)", + "LEDGER_ARCHIVE_NODE_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/archive:ledger-archive-node-canister-wasm)", + "SNS_WASM_CANISTER_WASM_PATH": "$(rootpath //rs/nns/sns-wasm:sns-wasm-canister)", + "SNS_GOVERNANCE_CANISTER_WASM_PATH": "$(rootpath //rs/sns/governance:sns-governance-canister)", + "SNS_ROOT_CANISTER_WASM_PATH": "$(rootpath //rs/sns/root:sns-root-canister)", + "SNS_SWAP_CANISTER_WASM_PATH": "$(rootpath //rs/sns/swap:sns-swap-canister)", + "ROOT_CANISTER_WASM_PATH": "$(rootpath //rs/nns/handlers/root/impl:root-canister)", + "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)", + "NNS_DAPP_WASM_PATH": "$(rootpath @nns_dapp_canister//file)", + "SNS_AGGREGATOR_WASM_PATH": "$(rootpath @sns_aggregator//file)", + "INTERNET_IDENTITY_WASM_PATH": "$(rootpath @ii_dev_canister//file)", + "SNS_TESTING_CANISTER_WASM_PATH": "$(rootpath :sns_testing_canister)", +} + +rust_binary( + name = "cli", + testonly = True, + srcs = ["src/main.rs"], + data = DEV_DATA, + env = DEV_ENV, + deps = DEPENDENCIES + [ + ":sns_testing", + ], +) + +rust_library( + name = "sns_testing", + testonly = True, + srcs = glob(["src/**/*.rs"]), + crate_name = "ic_sns_testing", + deps = DEPENDENCIES, +) + +rust_test( + name = "sns_testing_ci", + srcs = ["tests/sns_testing_ci.rs"], + data = DEV_DATA, + env = DEV_ENV, + deps = DEPENDENCIES + [ + ":sns_testing", + ], +) + +rust_canister( + name = "sns_testing_canister", + testonly = True, + srcs = ["canister/canister.rs"], + service_file = ":canister/test.did", + deps = [ + "@crate_index//:candid", + "@crate_index//:ic-cdk", + "@crate_index//:serde", + ], +) diff --git a/rs/sns/testing/Cargo.toml b/rs/sns/testing/Cargo.toml new file mode 100644 index 000000000000..08df81b2fb90 --- /dev/null +++ b/rs/sns/testing/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "ic-sns-testing" +version.workspace = true +authors.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true + +[[bin]] +name = "sns" +path = "src/main.rs" + +[[bin]] +name = "sns-testing-canister" +path = "canister/canister.rs" + +[lib] +path = "src/lib.rs" + +[dependencies] +candid = { workspace = true } +canister-test = { path = "../../rust_canisters/canister_test" } +clap = { workspace = true } +futures = { workspace = true } +ic-base-types = { path = "../../types/base_types" } +ic-cdk = { workspace = true } +ic-management-canister-types-private = { path = "../../types/management_canister_types" } +ic-nervous-system-agent = { path = "../../nervous_system/agent" } +ic-nervous-system-integration-tests = { path = "../../nervous_system/integration_tests" } +ic-nns-constants = { path = "../../nns/constants" } +ic-nns-common = { path = "../../nns/common" } +ic-nns-test-utils = { path = "../../nns/test_utils" } +ic-sns-governance-api = { path = "../../sns/governance/api" } +ic-sns-swap = { path = "../swap" } +pocket-ic = { path = "../../../packages/pocket-ic" } +reqwest = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } + diff --git a/rs/sns/testing/README.md b/rs/sns/testing/README.md new file mode 100644 index 000000000000..88acb84e644d --- /dev/null +++ b/rs/sns/testing/README.md @@ -0,0 +1,35 @@ +# SNS testing + +To run the scenario on the local PocketIC instance: +1) Launch PocketIC server: `bazel run //rs/pocket_ic_server:pocket-ic-server -- --ttl 6000 --port 8888` +2) Launch SNS testing scenario on it: `bazel run //rs/sns/testing:cli -- --server-url "http://127.0.0.1:8888"` + +Open local NNS dapp instance: http://qoctq-giaaa-aaaaa-aaaea-cai.localhost:8080/proposals/?u=qoctq-giaaa-aaaaa-aaaea-cai. +You should be able to see executed proposals to add SNS WASM to SNS-W canisters (since currently used NNS dapp is slightly outdated, make sure to clear topic filters). + +The scenario installs [test canister](./canister/canister.rs) and creates new SNS with it. +Once the proposal is adopted, the scenario initiates the SNS swap and closes it by providing sufficient amount of ICP. +Once swap is completed, the test canister is upgraded via SNS voting. + +NNS dapp should show the NNS proposal to create the new SNS as well as proposal in the newly created SNS to upgrade +the controlled canister. + +To interact with the network created by `sns-testing` CLI, you should add the following network config to +`~/.config/dfx/networks.json`: +``` +{ + "sns-testing": { + "bind": "127.0.0.1:8080" + } +} +``` + +Now you can call the testing canister by its id (note that the actual id may vary, make sure to check logs, or NNS proposal info in NNS dapp): +``` +dfx canister --network pocket-ic-system call mxqf3-4h777-77775-qaaaa-cai greet "IC" +``` + +To get the latest NNS proposal info: +``` +./dfx canister --network sns-testing call rrkah-fqaaa-aaaaa-aaaaq-cai list_proposals '(record { include_reward_status = vec {}; include_status = vec {}; exclude_topic = vec {}; limit = 1 })' +``` diff --git a/rs/sns/testing/canister/canister.rs b/rs/sns/testing/canister/canister.rs new file mode 100644 index 000000000000..bebda893f039 --- /dev/null +++ b/rs/sns/testing/canister/canister.rs @@ -0,0 +1,43 @@ +use candid::CandidType; +use serde::Deserialize; +use std::cell::RefCell; + +thread_local! { + static STR: RefCell = RefCell::new("Hoi".to_string()); +} + +#[derive(CandidType, Deserialize)] +pub struct InitArgs { + pub greeting: Option, +} + +fn init_impl(x: Option) { + match x { + None => (), + Some(x) => { + match x.greeting { + None => (), + Some(g) => { + STR.with(|s| *s.borrow_mut() = g); + } + }; + } + } +} + +#[ic_cdk::init] +fn init(x: Option) { + init_impl(x); +} + +#[ic_cdk::post_upgrade] +fn post_upgrade(x: Option) { + init_impl(x); +} + +#[ic_cdk::query] +fn greet(name: String) -> String { + format!("{}, {}!", STR.with(|s| (*s.borrow_mut()).clone()), name) +} + +fn main() {} diff --git a/rs/sns/testing/canister/test.did b/rs/sns/testing/canister/test.did new file mode 100644 index 000000000000..e1fe8ae13bec --- /dev/null +++ b/rs/sns/testing/canister/test.did @@ -0,0 +1,3 @@ +service : { + "greet": (text) -> (text); +} diff --git a/rs/sns/testing/src/lib.rs b/rs/sns/testing/src/lib.rs new file mode 100644 index 000000000000..bb7109e1c425 --- /dev/null +++ b/rs/sns/testing/src/lib.rs @@ -0,0 +1,2 @@ +pub mod nns_dapp; +pub mod sns; diff --git a/rs/sns/testing/src/main.rs b/rs/sns/testing/src/main.rs new file mode 100644 index 000000000000..08de1e7dd583 --- /dev/null +++ b/rs/sns/testing/src/main.rs @@ -0,0 +1,52 @@ +use clap::Parser; +use ic_sns_testing::nns_dapp::bootstrap_nns; +use ic_sns_testing::sns::{ + create_sns, install_test_canister, upgrade_sns_controlled_test_canister, TestCanisterInitArgs, +}; +use pocket_ic::PocketIcBuilder; +use reqwest::Url; + +#[derive(Debug, Parser)] +struct SnsTestingOpts { + #[arg(long)] + server_url: Url, +} + +#[tokio::main] +async fn main() { + let opts = SnsTestingOpts::parse(); + let mut pocket_ic = PocketIcBuilder::new() + .with_server_url(opts.server_url) + .with_nns_subnet() + .with_sns_subnet() + .with_ii_subnet() + .with_application_subnet() + .build_async() + .await; + let endpoint = pocket_ic.make_live(Some(8080)).await; + println!("PocketIC endpoint: {}", endpoint); + bootstrap_nns(&pocket_ic).await; + let greeting = "Hello there".to_string(); + let test_canister_id = install_test_canister( + &pocket_ic, + TestCanisterInitArgs { + greeting: Some(greeting), + }, + ) + .await; + println!("Test canister ID: {}", test_canister_id); + println!("Creating SNS..."); + let (sns, _nns_proposal_id) = create_sns(&pocket_ic, vec![test_canister_id]).await; + println!("SNS created"); + println!("Upgrading SNS-controlled test canister..."); + upgrade_sns_controlled_test_canister( + &pocket_ic, + sns, + test_canister_id, + TestCanisterInitArgs { + greeting: Some("Hi".to_string()), + }, + ) + .await; + println!("Test canister upgraded"); +} diff --git a/rs/sns/testing/src/nns_dapp.rs b/rs/sns/testing/src/nns_dapp.rs new file mode 100644 index 000000000000..ef27e0729a46 --- /dev/null +++ b/rs/sns/testing/src/nns_dapp.rs @@ -0,0 +1,192 @@ +use candid::{CandidType, Encode}; +use canister_test::Wasm; +use futures::future::join_all; +use ic_base_types::CanisterId; +use ic_nervous_system_agent::CallCanisters; +use ic_nervous_system_integration_tests::pocket_ic_helpers::{ + add_wasms_to_sns_wasm, install_canister_with_controllers, NnsInstaller, +}; +use ic_nns_constants::{ + CYCLES_MINTING_CANISTER_ID, GOVERNANCE_CANISTER_ID, IDENTITY_CANISTER_ID, LEDGER_CANISTER_ID, + LEDGER_INDEX_CANISTER_ID, LIFELINE_CANISTER_ID, NNS_UI_CANISTER_ID, REGISTRY_CANISTER_ID, + ROOT_CANISTER_ID, SNS_AGGREGATOR_CANISTER_ID, SNS_WASM_CANISTER_ID, +}; +use pocket_ic::nonblocking::PocketIc; + +const ALL_NNS_CANISTER_IDS: [&CanisterId; 8] = [ + &GOVERNANCE_CANISTER_ID, + &LEDGER_CANISTER_ID, + &ROOT_CANISTER_ID, + &LIFELINE_CANISTER_ID, + &SNS_WASM_CANISTER_ID, + ®ISTRY_CANISTER_ID, + &CYCLES_MINTING_CANISTER_ID, + &LEDGER_INDEX_CANISTER_ID, +]; + +async fn validate_subnet_setup(pocket_ic: &PocketIc) { + let topology = pocket_ic.topology().await; + let _nns_subnet_id = topology.get_nns().expect("NNS subnet not found"); + let _sns_subnet_id = topology.get_nns().expect("SNS subnet not found"); + let _ii_subnet_id = topology.get_ii().expect("II subnet not found"); + let app_subnet_ids = topology.get_app_subnets(); + assert!(!app_subnet_ids.is_empty(), "No application subnets found"); +} + +async fn check_canister_exists(pocket_ic: &PocketIc, canister_id: &CanisterId) -> bool { + pocket_ic + .canister_info(*canister_id) + .await + .map(|_| true) + .unwrap_or(false) +} + +pub async fn bootstrap_nns(pocket_ic: &PocketIc) { + // Ensure that all required subnets are present before proceeding to install NNS canisters + // At the moment this check doesn't make a lot of sense since we are always creating the new PocketIC instance + // with all the required subnets. However, in the future, we might want to be able to check externally provided + // networks. + validate_subnet_setup(pocket_ic).await; + + // Check if all NNS canisters are already installed + let canisters_exist = join_all( + ALL_NNS_CANISTER_IDS + .iter() + .map(|canister_id| async { check_canister_exists(pocket_ic, canister_id).await }), + ) + .await; + + if !canisters_exist.iter().any(|exists| *exists) { + // TODO @rvem: at some point in the future we might want to use + // non-default 'initial_balances' as well as 'neurons_fund_hotkeys' to provide + // tokens and neuron hotkeys for user-provided indentities. + let mut nns_installer = NnsInstaller::default(); + nns_installer.with_current_nns_canister_versions(); + nns_installer.with_test_governance_canister(); + nns_installer.with_cycles_minting_canister(); + nns_installer.with_index_canister(); + nns_installer.install(pocket_ic).await; + add_wasms_to_sns_wasm(pocket_ic, false).await.unwrap(); + } else if !canisters_exist.iter().all(|exists| *exists) { + panic!("Some NNS canisters are missing, we cannot fix this automatically at the moment"); + } + + install_frontend_nns_canisters(pocket_ic).await; +} + +#[derive(CandidType)] +struct SnsAggregatorPayload { + pub update_interval_ms: u64, + pub fast_interval_ms: u64, +} + +#[derive(CandidType)] +struct NnsDappPayload { + args: Vec<(String, String)>, +} + +async fn install_frontend_nns_canisters(pocket_ic: &PocketIc) { + let features = &[]; + + let sns_aggregator_wasm = + Wasm::from_location_specified_by_env_var("sns_aggregator", features).unwrap(); + let nns_dapp_wasm = Wasm::from_location_specified_by_env_var("nns_dapp", features).unwrap(); + let internet_identity_wasm = + Wasm::from_location_specified_by_env_var("internet_identity", features).unwrap(); + + if !check_canister_exists(pocket_ic, &SNS_AGGREGATOR_CANISTER_ID).await { + // Refresh every second so that the NNS dapp is as up-to-date as possible + let sns_aggregator_payload = SnsAggregatorPayload { + update_interval_ms: 1000, + fast_interval_ms: 100, + }; + + install_canister_with_controllers( + pocket_ic, + "sns_aggregator", + SNS_AGGREGATOR_CANISTER_ID, + Encode!(&sns_aggregator_payload).unwrap(), + sns_aggregator_wasm, + vec![ROOT_CANISTER_ID.get(), SNS_WASM_CANISTER_ID.get()], + ) + .await; + } + + if !check_canister_exists(pocket_ic, &IDENTITY_CANISTER_ID).await { + let internet_identity_payload: Option<()> = None; + + install_canister_with_controllers( + pocket_ic, + "internet-identity", + IDENTITY_CANISTER_ID, + Encode!(&internet_identity_payload).unwrap(), + internet_identity_wasm, + vec![ROOT_CANISTER_ID.get()], + ) + .await; + } + + if !check_canister_exists(pocket_ic, &NNS_UI_CANISTER_ID).await { + // TODO @rvem: perhaps, we may start using configurable endpoint for the IC http interface + // which should be considered in NNS dapp configuration. + let endpoint = "localhost:8080"; + let nns_dapp_payload = NnsDappPayload { + args: vec![ + ("API_HOST".to_string(), format!("http://{}", endpoint)), + ( + "CYCLES_MINTING_CANISTER_ID".to_string(), + CYCLES_MINTING_CANISTER_ID.get().to_string(), + ), + ("DFX_NETWORK".to_string(), "local".to_string()), + ( + "FEATURE_FLAGS".to_string(), + "{\"ENABLE_CKBTC\":false,\"ENABLE_CKTESTBTC\":false}".to_string(), + ), + ("FETCH_ROOT_KEY".to_string(), "true".to_string()), + ( + "GOVERNANCE_CANISTER_ID".to_string(), + GOVERNANCE_CANISTER_ID.get().to_string(), + ), + ("HOST".to_string(), format!("http://{}", endpoint)), + ( + "IDENTITY_SERVICE_URL".to_string(), + format!("http://{}.{}", IDENTITY_CANISTER_ID.get(), endpoint), + ), + ( + "LEDGER_CANISTER_ID".to_string(), + LEDGER_CANISTER_ID.get().to_string(), + ), + ( + "OWN_CANISTER_ID".to_string(), + NNS_UI_CANISTER_ID.get().to_string(), + ), + ( + "ROBOTS".to_string(), + "".to_string(), + ), + ( + "SNS_AGGREGATOR_URL".to_string(), + format!("http://{}.{}", SNS_AGGREGATOR_CANISTER_ID.get(), endpoint), + ), + ("STATIC_HOST".to_string(), format!("http://{}", endpoint)), + ( + "WASM_CANISTER_ID".to_string(), + SNS_WASM_CANISTER_ID.get().to_string(), + ), + ( + "INDEX_CANISTER_ID".to_string(), + LEDGER_INDEX_CANISTER_ID.get().to_string(), + ), + ], + }; + install_canister_with_controllers( + pocket_ic, + "nns-dapp", + NNS_UI_CANISTER_ID, + Encode!(&nns_dapp_payload).unwrap(), + nns_dapp_wasm, + vec![ROOT_CANISTER_ID.get()], + ) + .await; + }; +} diff --git a/rs/sns/testing/src/sns.rs b/rs/sns/testing/src/sns.rs new file mode 100644 index 000000000000..9235c3b3ddcf --- /dev/null +++ b/rs/sns/testing/src/sns.rs @@ -0,0 +1,118 @@ +use candid::{CandidType, Encode}; +use canister_test::Wasm; +use ic_base_types::CanisterId; +use ic_management_canister_types_private::CanisterInstallMode; +use ic_nervous_system_agent::sns::Sns; +use ic_nervous_system_integration_tests::{ + create_service_nervous_system_builder::CreateServiceNervousSystemBuilder, + pocket_ic_helpers::nns::governance::propose_to_deploy_sns_and_wait, + pocket_ic_helpers::sns::{ + governance::{ + propose_to_upgrade_sns_controlled_canister_and_wait, + EXPECTED_UPGRADE_DURATION_MAX_SECONDS, + }, + swap::{await_swap_lifecycle, smoke_test_participate_and_finalize}, + }, + pocket_ic_helpers::{await_with_timeout, install_canister_on_subnet}, +}; +use ic_nns_common::pb::v1::ProposalId; +use ic_nns_constants::ROOT_CANISTER_ID; +use ic_sns_governance_api::pb::v1::UpgradeSnsControlledCanister; +use ic_sns_swap::pb::v1::Lifecycle; +use pocket_ic::{management_canister::CanisterStatusResultStatus, nonblocking::PocketIc}; + +// TODO @rvem: I don't like the fact that this struct definition is copy-pasted from 'canister/canister.rs'. +// We should extract it into a separate crate and reuse in both canister and this crates. +#[derive(CandidType)] +pub struct TestCanisterInitArgs { + pub greeting: Option, +} + +pub async fn install_test_canister(pocket_ic: &PocketIc, args: TestCanisterInitArgs) -> CanisterId { + let topology = pocket_ic.topology().await; + let application_subnet_ids = topology.get_app_subnets(); + let application_subnet_id = application_subnet_ids + .first() + .expect("No Application subnet found"); + let features = &[]; + let test_canister_wasm = + Wasm::from_location_specified_by_env_var("sns_testing_canister", features).unwrap(); + install_canister_on_subnet( + pocket_ic, + *application_subnet_id, + Encode!(&args).unwrap(), + Some(test_canister_wasm), + vec![ROOT_CANISTER_ID.get()], + ) + .await +} + +pub async fn create_sns( + pocket_ic: &PocketIc, + dapp_canister_ids: Vec, +) -> (Sns, ProposalId) { + let sns_proposal_id = "1"; + let create_service_nervous_system = CreateServiceNervousSystemBuilder::default() + .neurons_fund_participation(true) + .with_dapp_canisters(dapp_canister_ids) + .build(); + let swap_parameters = create_service_nervous_system + .swap_parameters + .clone() + .unwrap(); + assert_eq!( + swap_parameters.start_time, None, + "Expecting the swap start time to be None to start the swap immediately" + ); + let (sns, proposal_id) = + propose_to_deploy_sns_and_wait(pocket_ic, create_service_nervous_system, sns_proposal_id) + .await; + await_swap_lifecycle(pocket_ic, sns.swap.canister_id, Lifecycle::Open) + .await + .expect("Expecting the swap to be open after creation"); + smoke_test_participate_and_finalize(pocket_ic, sns.swap.canister_id, swap_parameters).await; + await_swap_lifecycle(pocket_ic, sns.swap.canister_id, Lifecycle::Committed) + .await + .expect("Expecting the swap to be commited after creation and swap completion"); + (sns, proposal_id) +} + +pub async fn upgrade_sns_controlled_test_canister( + pocket_ic: &PocketIc, + sns: Sns, + canister_id: CanisterId, + upgrade_arg: TestCanisterInitArgs, +) { + // For now, we're using the same wasm module, but different init arguments used in 'post_upgrade' hook. + let features = &[]; + let test_canister_wasm = + Wasm::from_location_specified_by_env_var("sns_testing_canister", features).unwrap(); + propose_to_upgrade_sns_controlled_canister_and_wait( + pocket_ic, + sns.governance.canister_id, + UpgradeSnsControlledCanister { + canister_id: Some(canister_id.get()), + new_canister_wasm: test_canister_wasm.bytes(), + canister_upgrade_arg: Some(Encode!(&upgrade_arg).unwrap()), + mode: Some(CanisterInstallMode::Upgrade as i32), + chunked_canister_wasm: None, + }, + ) + .await; + // Wait for the canister to become available + await_with_timeout( + pocket_ic, + 0..EXPECTED_UPGRADE_DURATION_MAX_SECONDS, + |pocket_ic| async { + let canister_status = pocket_ic + .canister_status(canister_id.into(), Some(sns.root.canister_id.into())) + .await; + canister_status + .expect("Canister status is unavailable") + .status as u32 + }, + &(CanisterStatusResultStatus::Running as u32), + ) + .await + .expect("Test canister failed to get into the 'Running' state after upgrade"); +} diff --git a/rs/sns/testing/tests/sns_testing_ci.rs b/rs/sns/testing/tests/sns_testing_ci.rs new file mode 100644 index 000000000000..3084aa2b5a40 --- /dev/null +++ b/rs/sns/testing/tests/sns_testing_ci.rs @@ -0,0 +1,64 @@ +use candid::{Decode, Encode, Principal}; +use ic_sns_testing::nns_dapp::bootstrap_nns; +use ic_sns_testing::sns::{ + create_sns, install_test_canister, upgrade_sns_controlled_test_canister, TestCanisterInitArgs, +}; +use pocket_ic::PocketIcBuilder; + +#[tokio::test] +async fn test_sns_testing_pocket_ic() { + let pocket_ic = PocketIcBuilder::new() + .with_nns_subnet() + .with_sns_subnet() + .with_ii_subnet() + .with_application_subnet() + .build_async() + .await; + bootstrap_nns(&pocket_ic).await; + let greeting = "Hello there".to_string(); + let test_canister_id = install_test_canister( + &pocket_ic, + TestCanisterInitArgs { + greeting: Some(greeting.clone()), + }, + ) + .await; + let test_call_arg = "General Kenobi".to_string(); + let test_canister_response = pocket_ic + .query_call( + test_canister_id.into(), + Principal::anonymous(), + "greet", + Encode!(&test_call_arg).unwrap(), + ) + .await + .expect("Call to a test canister failed"); + assert_eq!( + Decode!(&test_canister_response, String).expect("Failed to decode test canister response"), + format!("{}, {}!", greeting, test_call_arg.clone()), + ); + let (sns, _nns_proposal_id) = create_sns(&pocket_ic, vec![test_canister_id]).await; + let new_greeting = "Hi".to_string(); + upgrade_sns_controlled_test_canister( + &pocket_ic, + sns, + test_canister_id, + TestCanisterInitArgs { + greeting: Some(new_greeting.clone()), + }, + ) + .await; + let test_canister_response = pocket_ic + .query_call( + test_canister_id.into(), + Principal::anonymous(), + "greet", + Encode!(&test_call_arg).unwrap(), + ) + .await + .expect("Call to a test canister failed"); + assert_eq!( + Decode!(&test_canister_response, String).expect("Failed to decode test canister response"), + format!("{}, {}!", new_greeting, test_call_arg), + ); +}