diff --git a/packages/pocket-ic/CHANGELOG.md b/packages/pocket-ic/CHANGELOG.md index 9e56594e5488..16e0664eed70 100644 --- a/packages/pocket-ic/CHANGELOG.md +++ b/packages/pocket-ic/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- The function `PocketIc::start_or_reuse_server` to manually start or reuse a PocketIC server. +- The function `start_server` and its input type `StartServerParams` to manually start a PocketIC server. - The function `PocketIcBuilder::with_all_icp_features` to specify that all ICP features (supported by PocketIC) should be enabled. diff --git a/packages/pocket-ic/src/common/rest.rs b/packages/pocket-ic/src/common/rest.rs index 4eee3a67cc24..8097f7e498e8 100644 --- a/packages/pocket-ic/src/common/rest.rs +++ b/packages/pocket-ic/src/common/rest.rs @@ -565,6 +565,7 @@ pub struct InstanceConfig { pub log_level: Option, pub bitcoind_addr: Option>, pub icp_features: Option, + pub allow_incomplete_state: Option, } #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)] diff --git a/packages/pocket-ic/src/lib.rs b/packages/pocket-ic/src/lib.rs index 9dad07ce57f3..c599bdd3b76b 100644 --- a/packages/pocket-ic/src/lib.rs +++ b/packages/pocket-ic/src/lib.rs @@ -81,14 +81,14 @@ use std::{ fs::OpenOptions, net::{IpAddr, SocketAddr}, path::PathBuf, - process::Command, + process::{Child, Command}, sync::{mpsc::channel, Arc}, thread, thread::JoinHandle, time::{Duration, SystemTime, UNIX_EPOCH}, }; use strum_macros::EnumIter; -use tempfile::TempDir; +use tempfile::{NamedTempFile, TempDir}; use thiserror::Error; use tokio::runtime::Runtime; use tracing::{instrument, warn}; @@ -617,8 +617,14 @@ impl PocketIc { let runtime = tokio::runtime::Builder::new_current_thread() .build() .unwrap(); - let url = runtime - .block_on(async { start_or_reuse_server(None).await.join("instances").unwrap() }); + let url = runtime.block_on(async { + let (_, server_url) = start_server(StartServerParams { + reuse: true, + ..Default::default() + }) + .await; + server_url.join("instances").unwrap() + }); let instances: Vec = reqwest::blocking::Client::new() .get(url) .send() @@ -1819,12 +1825,19 @@ async fn download_pocketic_server( Ok(()) } -/// Attempt to start a new PocketIC server if it's not already running. -pub async fn start_or_reuse_server(server_binary: Option) -> Url { +#[derive(Default)] +pub struct StartServerParams { + pub server_binary: Option, + /// Reuse an existing PocketIC server spawned by this process. + pub reuse: bool, +} + +/// Attempt to start a new PocketIC server. +pub async fn start_server(params: StartServerParams) -> (Child, Url) { let default_bin_dir = std::env::temp_dir().join(format!("pocket-ic-server-{}", EXPECTED_SERVER_VERSION)); let default_bin_path = default_bin_dir.join("pocket-ic"); - let mut bin_path: PathBuf = server_binary.unwrap_or_else(|| { + let mut bin_path: PathBuf = params.server_binary.unwrap_or_else(|| { std::env::var_os("POCKET_IC_BIN") .unwrap_or_else(|| default_bin_path.clone().into()) .into() @@ -1868,10 +1881,14 @@ pub async fn start_or_reuse_server(server_binary: Option) -> Url { } } - // We use the test driver's process ID to share the PocketIC server between multiple tests - // launched by the same test driver. - let test_driver_pid = std::process::id(); - let port_file_path = std::env::temp_dir().join(format!("pocket_ic_{}.port", test_driver_pid)); + let port_file_path = if params.reuse { + // We use the test driver's process ID to share the PocketIC server between multiple tests + // launched by the same test driver. + let test_driver_pid = std::process::id(); + std::env::temp_dir().join(format!("pocket_ic_{}.port", test_driver_pid)) + } else { + NamedTempFile::new().unwrap().into_temp_path().to_path_buf() + }; let mut cmd = pocket_ic_server_cmd(&bin_path); cmd.arg("--port-file"); #[cfg(windows)] @@ -1895,7 +1912,8 @@ pub async fn start_or_reuse_server(server_binary: Option) -> Url { // TODO: SDK-1936 #[allow(clippy::zombie_processes)] - cmd.spawn() + let child = cmd + .spawn() .unwrap_or_else(|_| panic!("Failed to start PocketIC binary ({})", bin_path.display())); loop { @@ -1905,7 +1923,10 @@ pub async fn start_or_reuse_server(server_binary: Option) -> Url { .trim_end() .parse() .expect("Failed to parse port to number"); - break Url::parse(&format!("http://{}:{}/", LOCALHOST, port)).unwrap(); + break ( + child, + Url::parse(&format!("http://{}:{}/", LOCALHOST, port)).unwrap(), + ); } } std::thread::sleep(Duration::from_millis(20)); diff --git a/packages/pocket-ic/src/nonblocking.rs b/packages/pocket-ic/src/nonblocking.rs index 9b6b5695973b..925210152cd4 100644 --- a/packages/pocket-ic/src/nonblocking.rs +++ b/packages/pocket-ic/src/nonblocking.rs @@ -11,8 +11,8 @@ use crate::common::rest::{ use crate::wsl_path; pub use crate::DefaultEffectiveCanisterIdError; use crate::{ - copy_dir, start_or_reuse_server, IngressStatusResult, PocketIcBuilder, PocketIcState, - RejectResponse, Time, + copy_dir, start_server, IngressStatusResult, PocketIcBuilder, PocketIcState, RejectResponse, + StartServerParams, Time, }; use backoff::backoff::Backoff; use backoff::{ExponentialBackoff, ExponentialBackoffBuilder}; @@ -144,7 +144,12 @@ impl PocketIc { let server_url = if let Some(server_url) = server_url { server_url } else { - start_or_reuse_server(server_binary).await + let (_, server_url) = start_server(StartServerParams { + server_binary, + reuse: true, + }) + .await; + server_url }; let subnet_config_set = subnet_config_set @@ -200,6 +205,7 @@ impl PocketIc { log_level: log_level.map(|l| l.to_string()), bitcoind_addr, icp_features: Some(icp_features), + allow_incomplete_state: Some(false), }; let test_driver_pid = std::process::id(); @@ -314,7 +320,12 @@ impl PocketIc { /// List all instances and their status. #[instrument(ret)] pub async fn list_instances() -> Vec { - let url = start_or_reuse_server(None).await.join("instances").unwrap(); + let (_, server_url) = start_server(StartServerParams { + reuse: true, + ..Default::default() + }) + .await; + let url = server_url.join("instances").unwrap(); let instances: Vec = reqwest::Client::new() .get(url) .send() diff --git a/packages/pocket-ic/tests/icp_features.rs b/packages/pocket-ic/tests/icp_features.rs index de7e79e7e812..a7caa104741c 100644 --- a/packages/pocket-ic/tests/icp_features.rs +++ b/packages/pocket-ic/tests/icp_features.rs @@ -1,6 +1,8 @@ use candid::{CandidType, Principal}; use pocket_ic::common::rest::{ExtendedSubnetConfigSet, IcpFeatures, InstanceConfig, SubnetSpec}; -use pocket_ic::{start_or_reuse_server, update_candid, PocketIc, PocketIcBuilder, PocketIcState}; +use pocket_ic::{ + start_server, update_candid, PocketIc, PocketIcBuilder, PocketIcState, StartServerParams, +}; use reqwest::StatusCode; use serde::Deserialize; use std::collections::BTreeMap; @@ -375,7 +377,7 @@ async fn with_all_icp_features_and_nns_subnet_state() { .unwrap() .into(); - let url = start_or_reuse_server(None).await; + let (_, url) = start_server(StartServerParams::default()).await; let client = reqwest::Client::new(); let instance_config = InstanceConfig { subnet_config_set: ExtendedSubnetConfigSet { @@ -387,6 +389,7 @@ async fn with_all_icp_features_and_nns_subnet_state() { log_level: None, bitcoind_addr: None, icp_features: Some(IcpFeatures::all_icp_features()), + allow_incomplete_state: None, }; let response = client .post(url.join("instances").unwrap()) diff --git a/packages/pocket-ic/tests/tests.rs b/packages/pocket-ic/tests/tests.rs index 9822c47efbd9..09591ad21bf9 100644 --- a/packages/pocket-ic/tests/tests.rs +++ b/packages/pocket-ic/tests/tests.rs @@ -18,6 +18,12 @@ use pocket_ic::{ query_candid, update_candid, DefaultEffectiveCanisterIdError, ErrorCode, IngressStatusResult, PocketIc, PocketIcBuilder, PocketIcState, RejectCode, Time, }; +#[cfg(not(windows))] +use pocket_ic::{ + common::rest::{CreateInstanceResponse, ExtendedSubnetConfigSet, InstanceConfig}, + nonblocking::PocketIc as PocketIcAsync, + start_server, StartServerParams, +}; use reqwest::blocking::Client; use reqwest::header::CONTENT_LENGTH; use reqwest::{Method, StatusCode}; @@ -25,7 +31,11 @@ use serde::Serialize; use sha2::{Digest, Sha256}; #[cfg(windows)] use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::{io::Read, sync::OnceLock, time::SystemTime}; +use std::{ + io::Read, + sync::OnceLock, + time::{Duration, SystemTime}, +}; use tempfile::{NamedTempFile, TempDir}; #[cfg(windows)] use wslpath::windows_to_wsl; @@ -426,6 +436,126 @@ fn set_time_into_past() { pic.set_time(now.into()); } +#[test] +fn time_on_resumed_instance() { + let state = PocketIcState::new(); + + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_state(state) + .build(); + + let now = SystemTime::now(); + pic.set_certified_time(now.into()); + + let time = pic.get_time(); + assert_eq!(time, now.into()); + let state = pic.drop_and_take_state().unwrap(); + + let pic = PocketIcBuilder::new().with_state(state).build(); + + // The time on the resumed instances increases by 2ns: + // - 1ns due to executing a checkpointed round before dropping the original instance; + // - 1ns due to bumping time when creating a new instance to ensure strict time monotonicity. + let resumed_time = pic.get_time(); + assert_eq!(resumed_time, time + Duration::from_nanos(2)); +} + +// Killing the PocketIC server inside WSL is challenging => skipping this test on Windows. +#[cfg(not(windows))] +async fn resume_killed_instance_impl(allow_incomplete_state: Option) -> Result<(), String> { + let (mut server, server_url) = start_server(StartServerParams::default()).await; + let temp_dir = TempDir::new().unwrap(); + + let state = PocketIcState::new_from_path(temp_dir.path().to_path_buf()); + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_server_url(server_url) + .with_state(state) + .build_async() + .await; + + let canister_id = pic.create_canister().await; + + // Execute many rounds to trigger a checkpoint. + for _ in 0..600 { + pic.tick().await; + } + + // The following (most recent) changes will be lost after killing the instance. + let now = SystemTime::now(); + pic.set_certified_time(now.into()).await; + let another_canister_id = pic.create_canister().await; + + assert!(pic.canister_exists(canister_id).await); + assert!(pic.canister_exists(another_canister_id).await); + let time = pic.get_time().await; + assert!(time >= now.into()); + + server.kill().unwrap(); + + let (_, server_url) = start_server(StartServerParams::default()).await; + let client = reqwest::Client::new(); + let instance_config = InstanceConfig { + subnet_config_set: ExtendedSubnetConfigSet::default(), + state_dir: Some(temp_dir.path().to_path_buf()), + nonmainnet_features: false, + log_level: None, + bitcoind_addr: None, + icp_features: None, + allow_incomplete_state, + }; + let response = client + .post(server_url.join("instances").unwrap()) + .json(&instance_config) + .send() + .await + .unwrap(); + if !response.status().is_success() { + return Err(response.text().await.unwrap()); + } + let instance_id = match response.json::().await.unwrap() { + CreateInstanceResponse::Created { instance_id, .. } => instance_id, + CreateInstanceResponse::Error { message } => panic!("Unexpected error: {}", message), + }; + let pic = PocketIcAsync::new_from_existing_instance(server_url, instance_id, None); + + // Only the first canister (created before the last checkpoint) is preserved, + // the other canister and time change are lost. + assert!(pic.canister_exists(canister_id).await); + assert!(!pic.canister_exists(another_canister_id).await); + let resumed_time = pic.get_time().await; + assert!(resumed_time < now.into()); + + // Drop instance explicitly to prevent data races in the StateManager. + pic.drop().await; + + Ok(()) +} + +// Killing the PocketIC server inside WSL is challenging => skipping this test on Windows. +#[cfg(not(windows))] +#[tokio::test] +async fn resume_killed_instance_default() { + let err = resume_killed_instance_impl(None).await.unwrap_err(); + assert!(err.contains("The state of subnet with seed 7712b2c09cb96b3aa3fbffd4034a21a39d5d13f80e043161d1d71f4c593434af is incomplete.")); +} + +// Killing the PocketIC server inside WSL is challenging => skipping this test on Windows. +#[cfg(not(windows))] +#[tokio::test] +async fn resume_killed_instance_strict() { + let err = resume_killed_instance_impl(Some(false)).await.unwrap_err(); + assert!(err.contains("The state of subnet with seed 7712b2c09cb96b3aa3fbffd4034a21a39d5d13f80e043161d1d71f4c593434af is incomplete.")); +} + +// Killing the PocketIC server inside WSL is challenging => skipping this test on Windows. +#[cfg(not(windows))] +#[tokio::test] +async fn resume_killed_instance() { + resume_killed_instance_impl(Some(true)).await.unwrap(); +} + #[test] fn test_get_set_cycle_balance() { let pic = PocketIc::new(); diff --git a/rs/pocket_ic_server/CHANGELOG.md b/rs/pocket_ic_server/CHANGELOG.md index d5a4d264081c..da364911198c 100644 --- a/rs/pocket_ic_server/CHANGELOG.md +++ b/rs/pocket_ic_server/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The endpoint `/instances//update/await_ingress_message` (execute rounds on the PocketIc instance until the message is executed): to fix a performance regression when using the two endpoints `/instances//update/tick` and `/instances//read/ingress_status` in a loop. - The argument of the endpoint `/instances/` takes an additional optional field `icp_features` specifying ICP features (implemented by system canisters) to be enabled in the newly created PocketIC instance. +- The argument of the endpoint `/instances/` takes an additional optional field `allow_incomplete_state` specifying if incomplete state (e.g., resulting from not deleting a PocketIC instance gracefully) is allowed. diff --git a/rs/pocket_ic_server/src/pocket_ic.rs b/rs/pocket_ic_server/src/pocket_ic.rs index 9f3b251b51af..48401b1dfb7b 100644 --- a/rs/pocket_ic_server/src/pocket_ic.rs +++ b/rs/pocket_ic_server/src/pocket_ic.rs @@ -99,6 +99,7 @@ use pocket_ic::{copy_dir, ErrorCode, RejectCode, RejectResponse}; use registry_canister::init::RegistryCanisterInitPayload; use serde::{Deserialize, Serialize}; use slog::Level; +use std::cmp::max; use std::hash::Hash; use std::str::FromStr; use std::{ @@ -201,15 +202,10 @@ fn compute_subnet_seed( #[derive(Clone, Deserialize, Serialize)] struct RawTopologyInternal { - pub subnet_configs: Vec, + pub subnet_configs: Vec, pub default_effective_canister_id: RawCanisterId, pub icp_features: Option, pub synced_registry_version: Option, -} - -#[derive(Clone, Deserialize, Serialize)] -struct RawSubnetConfigInternal { - pub subnet_config: SubnetConfigInternal, pub time: SystemTime, } @@ -491,7 +487,6 @@ impl PocketIcSubnets { instruction_config: SubnetInstructionConfig, registry_data_provider: Arc, create_at_registry_version: RegistryVersion, - time: SystemTime, nonmainnet_features: bool, log_level: Option, bitcoin_adapter_uds_path: Option, @@ -537,18 +532,12 @@ impl PocketIcSubnets { .feature_flags .rate_limiting_of_debug_prints = FlagStatus::Disabled; let state_machine_config = StateMachineConfig::new(subnet_config, hypervisor_config); - let t = time - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64; - let time = Time::from_nanos_since_unix_epoch(t); StateMachineBuilder::new() .with_runtime(runtime) .with_config(Some(state_machine_config)) .with_subnet_seed(subnet_seed) .with_subnet_size(subnet_size.try_into().unwrap()) .with_subnet_type(subnet_type) - .with_time(time) .with_state_machine_state_dir(state_machine_state_dir) .with_registry_data_provider(registry_data_provider.clone()) .with_log_level(log_level) @@ -591,6 +580,21 @@ impl PocketIcSubnets { } } + fn persist_topology(&self, default_effective_canister_id: Principal) { + if let Some(ref state_dir) = self.state_dir { + let raw_topology: RawTopologyInternal = RawTopologyInternal { + subnet_configs: self.subnet_configs.clone(), + default_effective_canister_id: default_effective_canister_id.into(), + icp_features: self.icp_features.clone(), + synced_registry_version: Some(self.synced_registry_version.get()), + time: self.time(), + }; + let topology_json = serde_json::to_string(&raw_topology).unwrap(); + let mut topology_file = File::create(state_dir.join("topology.json")).unwrap(); + topology_file.write_all(topology_json.as_bytes()).unwrap(); + } + } + fn get_all(&self) -> Vec> { self.subnets.get_all() } @@ -620,7 +624,10 @@ impl PocketIcSubnets { .unwrap_or(GENESIS.into()) } - fn create_subnet(&mut self, subnet_config_info: SubnetConfigInfo) -> SubnetConfigInternal { + fn create_subnet( + &mut self, + subnet_config_info: SubnetConfigInfo, + ) -> Result { let SubnetConfigInfo { ranges, alloc_range, @@ -628,19 +635,9 @@ impl PocketIcSubnets { subnet_state_dir, subnet_kind, instruction_config, - mut time, + time, } = subnet_config_info; - // All subnets must eventually have the same time and time can only advance => - // advance time of the new subnet if other subnets have higher time; - // the maximum time must be determined before adding a `StateMachine` - // for the new subnet to `self.subnets` because `self.time()` - // is only sound if all subnets in `self.subnets` have the same time. - let current_time = self.time(); - if current_time > time { - time = current_time; - } - let subnet_seed = compute_subnet_seed(ranges.clone(), alloc_range); let state_machine_state_dir: Box = @@ -672,7 +669,6 @@ impl PocketIcSubnets { instruction_config.clone(), self.registry_data_provider.clone(), create_at_registry_version, - time, self.nonmainnet_features, self.log_level, bitcoin_adapter_uds_path.clone(), @@ -722,6 +718,16 @@ impl PocketIcSubnets { // if one was provided). let subnet_id = sm.get_subnet_id(); + if let Some(expected_time) = time { + let actual_time: SystemTime = sm.get_state_time().into(); + if actual_time != expected_time { + return Err(format!( + "The state of subnet with seed {} is incomplete.", + hex::encode(subnet_seed) + )); + } + } + // The subnet created first is marked as the NNS subnet. if self.nns_subnet.is_none() { self.nns_subnet = Some(self.subnets.get_subnet(subnet_id).unwrap()); @@ -787,6 +793,13 @@ impl PocketIcSubnets { subnet.state_machine.reload_registry(); } + // All subnets must have the same time and time can only advance => + // set the time to the maximum time in the latest state across all subnets. + let mut time: SystemTime = GENESIS.into(); + for subnet in self.subnets.get_all() { + time = max(time, subnet.state_machine.get_state_time().into()); + } + // Make sure time is strictly monotone. time += Duration::from_nanos(1); @@ -836,7 +849,7 @@ impl PocketIcSubnets { } } - subnet_config + Ok(subnet_config) } fn get_nns(&self) -> Option> { @@ -1232,7 +1245,7 @@ pub struct PocketIc { impl Drop for PocketIc { fn drop(&mut self) { - if let Some(ref state_dir) = self.subnets.state_dir { + if self.subnets.state_dir.is_some() { let subnets = self.subnets.get_all(); for subnet in &subnets { subnet.state_machine.checkpointed_tick(); @@ -1240,27 +1253,8 @@ impl Drop for PocketIc { for subnet in &subnets { subnet.state_machine.await_state_hash(); } - let subnet_configs = self - .subnets - .subnet_configs - .iter() - .map(|config| { - let time = self.subnets.get(config.subnet_id).unwrap().time(); - RawSubnetConfigInternal { - subnet_config: config.clone(), - time, - } - }) - .collect(); - let raw_topology: RawTopologyInternal = RawTopologyInternal { - subnet_configs, - default_effective_canister_id: self.default_effective_canister_id.into(), - icp_features: self.subnets.icp_features.clone(), - synced_registry_version: Some(self.subnets.synced_registry_version.get()), - }; - let topology_json = serde_json::to_string(&raw_topology).unwrap(); - let mut topology_file = File::create(state_dir.join("topology.json")).unwrap(); - topology_file.write_all(topology_json.as_bytes()).unwrap(); + self.subnets + .persist_topology(self.default_effective_canister_id); } for subnet in self.subnets.get_all() { subnet.state_machine.drop_payload_builder(); @@ -1337,6 +1331,7 @@ impl PocketIc { log_level: Option, bitcoind_addr: Option>, icp_features: Option, + allow_incomplete_state: Option, ) -> Result { if let Some(ref icp_features) = icp_features { subnet_configs = subnet_configs.try_with_icp_features(icp_features)?; @@ -1379,20 +1374,23 @@ impl PocketIc { .subnet_configs .into_iter() .map(|config| { - range_gen - .add_assigned(config.subnet_config.ranges.clone()) - .unwrap(); - if let Some(allocation_range) = config.subnet_config.alloc_range { + range_gen.add_assigned(config.ranges.clone()).unwrap(); + if let Some(allocation_range) = config.alloc_range { range_gen.add_assigned(vec![allocation_range]).unwrap(); } + let time = if let Some(true) = allow_incomplete_state { + None + } else { + Some(topology.time) + }; SubnetConfigInfo { - ranges: config.subnet_config.ranges, - alloc_range: config.subnet_config.alloc_range, - subnet_id: Some(config.subnet_config.subnet_id), + ranges: config.ranges, + alloc_range: config.alloc_range, + subnet_id: Some(config.subnet_id), subnet_state_dir: None, - subnet_kind: config.subnet_config.subnet_kind, - instruction_config: config.subnet_config.instruction_config, - time: config.time, + subnet_kind: config.subnet_kind, + instruction_config: config.instruction_config, + time, } }) .collect() @@ -1440,7 +1438,7 @@ impl PocketIc { let mut subnet_config_info: Vec = vec![]; for (subnet_kind, subnet_state_dir, instruction_config) in all_subnets { - let (ranges, alloc_range, subnet_id, time) = if let Some(ref subnet_state_dir) = + let (ranges, alloc_range, subnet_id) = if let Some(ref subnet_state_dir) = subnet_state_dir { match std::fs::read_dir(subnet_state_dir) { @@ -1490,7 +1488,6 @@ impl PocketIc { }; let subnet_id = metadata.own_subnet_id; - let time = metadata.batch_time; let ranges: Vec<_> = metadata .network_topology .routing_table @@ -1525,14 +1522,14 @@ impl PocketIc { } } - (ranges, None, Some(subnet_id), time) + (ranges, None, Some(subnet_id)) } else { let RangeConfig { canister_id_ranges: ranges, canister_allocation_range: alloc_range, } = get_range_config(subnet_kind, &mut range_gen)?; - (ranges, alloc_range, None, GENESIS) + (ranges, alloc_range, None) }; subnet_config_info.push(SubnetConfigInfo { @@ -1542,7 +1539,7 @@ impl PocketIc { subnet_state_dir, subnet_kind, instruction_config, - time: time.into(), + time: None, }); } @@ -1568,7 +1565,7 @@ impl PocketIc { ); let mut subnet_configs = Vec::new(); for subnet_config_info in subnet_config_info.into_iter() { - let subnet_config_internal = subnets.create_subnet(subnet_config_info); + let subnet_config_internal = subnets.create_subnet(subnet_config_info)?; subnet_configs.push(subnet_config_internal); } @@ -1596,6 +1593,8 @@ impl PocketIc { }) .default_effective_canister_id(); + subnets.persist_topology(default_effective_canister_id); + let state_label = StateLabel::new(seed); Ok(Self { @@ -1790,7 +1789,7 @@ struct SubnetConfigInfo { pub subnet_state_dir: Option, pub subnet_kind: SubnetKind, pub instruction_config: SubnetInstructionConfig, - pub time: SystemTime, + pub time: Option, } // ---------------------------------------------------------------------------------------- // @@ -3368,8 +3367,10 @@ fn route( subnet_state_dir: None, subnet_kind, instruction_config, - time: GENESIS.into(), - }); + time: None, + })?; + pic.subnets + .persist_topology(pic.default_effective_canister_id); Ok(pic.try_route_canister(canister_id).unwrap()) } else { // If the request is not an update call to create a canister using the provisional API, @@ -3473,6 +3474,7 @@ mod tests { None, None, None, + None, ) .unwrap(); let mut pic1 = PocketIc::try_new( @@ -3487,6 +3489,7 @@ mod tests { None, None, None, + None, ) .unwrap(); assert_ne!(pic0.get_state_label(), pic1.get_state_label()); diff --git a/rs/pocket_ic_server/src/state_api/routes.rs b/rs/pocket_ic_server/src/state_api/routes.rs index da8717c16f3a..4606c8aee0ab 100644 --- a/rs/pocket_ic_server/src/state_api/routes.rs +++ b/rs/pocket_ic_server/src/state_api/routes.rs @@ -1213,6 +1213,7 @@ pub async fn create_instance( log_level, instance_config.bitcoind_addr, instance_config.icp_features, + instance_config.allow_incomplete_state, ) }) .await diff --git a/rs/pocket_ic_server/tests/test.rs b/rs/pocket_ic_server/tests/test.rs index b3d020559382..371f1afff8a9 100644 --- a/rs/pocket_ic_server/tests/test.rs +++ b/rs/pocket_ic_server/tests/test.rs @@ -130,6 +130,7 @@ fn test_creation_of_instance_extended() { log_level: None, bitcoind_addr: None, icp_features: None, + allow_incomplete_state: None, }; let response = client .post(url.join("instances").unwrap())