From 1b55fb821cc82b6cb005988bf0a2c04e00f67f1c Mon Sep 17 00:00:00 2001 From: veeso Date: Thu, 15 Jun 2023 16:44:22 +0200 Subject: [PATCH] EPROD-381 integration tests for stable structures --- .github/workflows/build_and_tests.yml | 27 +- Cargo.toml | 2 + dfx.json | 86 +++--- ic-payments/build_payments_canister.sh | 17 ++ ic-payments/state_machine_test.sh | 18 +- ic-stable-structures/Cargo.toml | 13 + ic-stable-structures/src/error.rs | 17 +- .../src/storage/structures/vec.rs | 2 +- .../src/storage_wasm/structures_wasm.rs | 4 +- ic-stable-structures/tests/did/Cargo.toml | 12 + ic-stable-structures/tests/did/src/lib.rs | 36 +++ .../tests/dummy_canister/.gitignore | 1 + .../tests/dummy_canister/Cargo.toml | 20 ++ .../tests/dummy_canister/dummy_canister.did | 15 + .../tests/dummy_canister/src/canister.rs | 86 ++++++ .../dummy_canister/src/canister/service.rs | 175 ++++++++++++ .../tests/dummy_canister/src/main.rs | 11 + .../tests/integration_test.rs | 4 + .../tests/state_machine/btreemap.rs | 39 +++ .../tests/state_machine/cell.rs | 39 +++ .../tests/state_machine/log.rs | 40 +++ .../tests/state_machine/map.rs | 40 +++ .../tests/state_machine/mod.rs | 258 ++++++++++++++++++ .../tests/state_machine/multimap.rs | 40 +++ .../tests/state_machine/vec.rs | 40 +++ ic-stable-structures/tests/utils/mod.rs | 13 + ic-stable-structures/tests/utils/wasm.rs | 56 ++++ scripts/build.sh | 3 + scripts/build_dummy_canister.sh | 6 + 29 files changed, 1060 insertions(+), 60 deletions(-) create mode 100755 ic-payments/build_payments_canister.sh create mode 100644 ic-stable-structures/tests/did/Cargo.toml create mode 100644 ic-stable-structures/tests/did/src/lib.rs create mode 100644 ic-stable-structures/tests/dummy_canister/.gitignore create mode 100644 ic-stable-structures/tests/dummy_canister/Cargo.toml create mode 100644 ic-stable-structures/tests/dummy_canister/dummy_canister.did create mode 100644 ic-stable-structures/tests/dummy_canister/src/canister.rs create mode 100644 ic-stable-structures/tests/dummy_canister/src/canister/service.rs create mode 100644 ic-stable-structures/tests/dummy_canister/src/main.rs create mode 100644 ic-stable-structures/tests/integration_test.rs create mode 100644 ic-stable-structures/tests/state_machine/btreemap.rs create mode 100644 ic-stable-structures/tests/state_machine/cell.rs create mode 100644 ic-stable-structures/tests/state_machine/log.rs create mode 100644 ic-stable-structures/tests/state_machine/map.rs create mode 100644 ic-stable-structures/tests/state_machine/mod.rs create mode 100644 ic-stable-structures/tests/state_machine/multimap.rs create mode 100644 ic-stable-structures/tests/state_machine/vec.rs create mode 100644 ic-stable-structures/tests/utils/mod.rs create mode 100644 ic-stable-structures/tests/utils/wasm.rs create mode 100755 scripts/build_dummy_canister.sh diff --git a/.github/workflows/build_and_tests.yml b/.github/workflows/build_and_tests.yml index 2a7f4db8..aa17ba9d 100644 --- a/.github/workflows/build_and_tests.yml +++ b/.github/workflows/build_and_tests.yml @@ -57,4 +57,29 @@ jobs: - name: Generate code coverage run: | - cargo +nightly tarpaulin --verbose --timeout 120 --out Xml --workspace --exclude canister-a --exclude canister-b --exclude canister-c --exclude canister-d --exclude canister-e --exclude test-payment-canister + cargo +nightly tarpaulin --verbose --timeout 120 --out Xml --workspace --exclude canister-a --exclude canister-b --exclude canister-c --exclude canister-d --exclude canister-e --exclude test-payment-canister + + integration-test: + name: Integration tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Machine setup + run: | + sudo apt update + sudo apt install -y libunwind-dev cmake protobuf-compiler + sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" + rustup target add wasm32-unknown-unknown + cargo install ic-wasm + dfx start --background + dfx canister create dummy_canister + dfx build dummy_canister + ic-payments/build_payments_canister.sh + + - name: Run integration tests + run: | + export WASMS_DIR="../target/wasm32-unknown-unknown/release/" + cd ./ic-stable-structures + cargo test --features state-machine diff --git a/Cargo.toml b/Cargo.toml index 647594ed..ba645516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ members = [ "ic-payments", "ic-payments/test-payment-canister", "ic-stable-structures", + "ic-stable-structures/tests/did", + "ic-stable-structures/tests/dummy_canister", "ic-storage", "ic-storage/ic-storage-derive", ] diff --git a/dfx.json b/dfx.json index 48c7e587..7403e1f5 100644 --- a/dfx.json +++ b/dfx.json @@ -1,41 +1,47 @@ { - "canisters": { - "canister-a": { - "build": "bash scripts/build.sh", - "candid": "ic-canister/tests/canister_a/canister_a.did", - "wasm": "target/wasm32-unknown-unknown/release/canister_a.wasm", - "type": "custom" - }, - "canister-b": { - "build": "bash scripts/build.sh", - "candid": "ic-canister/tests/canister_b/canister_b.did", - "wasm": "target/wasm32-unknown-unknown/release/canister_b.wasm", - "type": "custom" - }, - "canister-c": { - "build": "bash scripts/build.sh", - "candid": "ic-canister/tests/canister-c/canister-c.did", - "wasm": "target/wasm32-unknown-unknown/release/canister-c.wasm", - "type": "custom" - }, - "canister-d": { - "build": "bash scripts/build.sh", - "candid": "ic-canister/tests/canister-d/canister-d.did", - "wasm": "target/wasm32-unknown-unknown/release/canister-d.wasm", - "type": "custom" - }, - "log_canister": { - "candid": "ic-log/examples/log_canister.did", - "build": "bash scripts/build_log_canister.sh", - "wasm": "target/wasm32-unknown-unknown/release/examples/log_canister.wasm", - "type": "custom" - } - }, - "networks": { - "local": { - "bind": "127.0.0.1:8000", - "type": "ephemeral" - } - }, - "version": 1 -} \ No newline at end of file + "canisters": { + "canister-a": { + "build": "bash scripts/build.sh", + "candid": "ic-canister/tests/canister_a/canister_a.did", + "wasm": "target/wasm32-unknown-unknown/release/canister_a.wasm", + "type": "custom" + }, + "canister-b": { + "build": "bash scripts/build.sh", + "candid": "ic-canister/tests/canister_b/canister_b.did", + "wasm": "target/wasm32-unknown-unknown/release/canister_b.wasm", + "type": "custom" + }, + "canister-c": { + "build": "bash scripts/build.sh", + "candid": "ic-canister/tests/canister-c/canister-c.did", + "wasm": "target/wasm32-unknown-unknown/release/canister-c.wasm", + "type": "custom" + }, + "canister-d": { + "build": "bash scripts/build.sh", + "candid": "ic-canister/tests/canister-d/canister-d.did", + "wasm": "target/wasm32-unknown-unknown/release/canister-d.wasm", + "type": "custom" + }, + "dummy_canister": { + "candid": "ic-stable-structures/tests/dummy_canister/dummy_canister.did", + "build": "bash scripts/build_dummy_canister.sh", + "wasm": "target/wasm32-unknown-unknown/release/dummy_canister.wasm", + "type": "custom" + }, + "log_canister": { + "candid": "ic-log/examples/log_canister.did", + "build": "bash scripts/build_log_canister.sh", + "wasm": "target/wasm32-unknown-unknown/release/examples/log_canister.wasm", + "type": "custom" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + }, + "version": 1 +} diff --git a/ic-payments/build_payments_canister.sh b/ic-payments/build_payments_canister.sh new file mode 100755 index 00000000..7f6c433e --- /dev/null +++ b/ic-payments/build_payments_canister.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +export PROTOC_INCLUDE=${PWD}/../ + +# Get example icrc1 canister +if [ ! -f ic-payments/tests/common/ic-icrc1-ledger.wasm ]; then + export IC_VERSION=b43543ce7365acd1720294e701e8e8361fa30c8f + curl -o ic-icrc1-ledger.wasm.gz https://download.dfinity.systems/ic/${IC_VERSION}/canisters/ic-icrc1-ledger.wasm.gz + gunzip ic-icrc1-ledger.wasm.gz + mv ic-icrc1-ledger.wasm ic-payments/tests/common/ +fi + +# Build test payment canister +cargo build --target wasm32-unknown-unknown --features export-api -p test-payment-canister --release +ic-wasm target/wasm32-unknown-unknown/release/test_payment_canister.wasm -o ic-payments/tests/common/payment_canister.wasm shrink diff --git a/ic-payments/state_machine_test.sh b/ic-payments/state_machine_test.sh index f3d35531..21fb3921 100755 --- a/ic-payments/state_machine_test.sh +++ b/ic-payments/state_machine_test.sh @@ -1,22 +1,10 @@ +#!/bin/sh + # Example script to run the state_machine tests. To make it work, replace # the location of google protobuf repo in your system. You can find the # needed repo at https://github.com/protocolbuffers/protobuf/tree/main/src/google/protobuf -set -e - -export PROTOC_INCLUDE=${PWD}/../ - -# Get example icrc1 canister -if [ ! -f ic-payments/tests/common/ic-icrc1-ledger.wasm ]; then - export IC_VERSION=b43543ce7365acd1720294e701e8e8361fa30c8f - curl -o ic-icrc1-ledger.wasm.gz https://download.dfinity.systems/ic/${IC_VERSION}/canisters/ic-icrc1-ledger.wasm.gz - gunzip ic-icrc1-ledger.wasm.gz - mv ic-icrc1-ledger.wasm ic-payments/tests/common/ -fi - -# Build test payment canister -cargo build --target wasm32-unknown-unknown --features export-api -p test-payment-canister --release -ic-wasm target/wasm32-unknown-unknown/release/test_payment_canister.wasm -o ic-payments/tests/common/payment_canister.wasm shrink +./build_payments_canister.sh # Run the test cargo +nightly test -p ic-payments --features state-machine diff --git a/ic-stable-structures/Cargo.toml b/ic-stable-structures/Cargo.toml index f7f5700b..a7cd8df5 100644 --- a/ic-stable-structures/Cargo.toml +++ b/ic-stable-structures/Cargo.toml @@ -6,3 +6,16 @@ edition.workspace = true [dependencies] ic-exports = { path = "../ic-exports" } thiserror = { workspace = true } + +[dev-dependencies] +anyhow = "1" +candid = { workspace = true } +did = { path = "./tests/did" } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +once_cell = "1.18" +serde = { workspace = true } + + +[features] +state-machine = ["ic-exports/state-machine"] diff --git a/ic-stable-structures/src/error.rs b/ic-stable-structures/src/error.rs index 9e40095d..5c107adb 100644 --- a/ic-stable-structures/src/error.rs +++ b/ic-stable-structures/src/error.rs @@ -1,4 +1,4 @@ -use ic_exports::stable_structures::{btreemap, cell, log, GrowFailed}; +use ic_exports::stable_structures::{btreemap, cell, log, vec, GrowFailed}; use thiserror::Error; pub type Result = std::result::Result; @@ -11,6 +11,10 @@ pub enum Error { ValueTooLarge(u64), #[error("memory manager and stable structure has incompatible versions")] IncompatibleVersions, + #[error("the vector type is not compatible with the current vector")] + IncompatibleElementType, + #[error("bad magic number: {0:?}")] + BadMagic([u8; 3]), } impl From for Error { @@ -46,6 +50,17 @@ impl From for Error { } } +impl From for Error { + fn from(e: vec::InitError) -> Self { + match e { + vec::InitError::IncompatibleVersion(_) => Self::IncompatibleVersions, + vec::InitError::IncompatibleElementType => Self::IncompatibleElementType, + vec::InitError::OutOfMemory => Self::OutOfStableMemory, + vec::InitError::BadMagic(magic) => Self::BadMagic(magic), + } + } +} + impl From for Error { fn from(_: GrowFailed) -> Self { Self::OutOfStableMemory diff --git a/ic-stable-structures/src/storage/structures/vec.rs b/ic-stable-structures/src/storage/structures/vec.rs index 4870e7ab..16662b0b 100644 --- a/ic-stable-structures/src/storage/structures/vec.rs +++ b/ic-stable-structures/src/storage/structures/vec.rs @@ -29,7 +29,7 @@ impl StableVec { self.get_inner().map_or(true, InnerVec::is_empty) } - /// Removes al the values from the vector + /// Removes all the values from the vector pub fn clear(&mut self) -> Result<()> { let memory_id = self.memory_id; if let Some(vec) = self.mut_inner() { diff --git a/ic-stable-structures/src/storage_wasm/structures_wasm.rs b/ic-stable-structures/src/storage_wasm/structures_wasm.rs index c527fbd1..c8d1ea61 100644 --- a/ic-stable-structures/src/storage_wasm/structures_wasm.rs +++ b/ic-stable-structures/src/storage_wasm/structures_wasm.rs @@ -192,7 +192,7 @@ impl StableLog { let index_memory = crate::get_memory_by_id(index_memory_id); let data_memory = crate::get_memory_by_id(data_memory_id); - let inner = log::Log::new(index_memory, data_memory); + let inner = log::Log::init(index_memory, data_memory)?; Ok(Self(Some(inner))) } @@ -308,7 +308,7 @@ impl StableVec { /// Creates new `StableVec` pub fn new(memory_id: MemoryId) -> Result { Ok(Self( - vec::Vec::::new(get_memory_by_id(memory_id))?, + vec::Vec::::init(get_memory_by_id(memory_id))?, memory_id, )) } diff --git a/ic-stable-structures/tests/did/Cargo.toml b/ic-stable-structures/tests/did/Cargo.toml new file mode 100644 index 00000000..6cca0ab8 --- /dev/null +++ b/ic-stable-structures/tests/did/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "did" +version.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +candid = { workspace = true } +ic-stable-structures = { path = "../../../ic-stable-structures" } +serde = { workspace = true } diff --git a/ic-stable-structures/tests/did/src/lib.rs b/ic-stable-structures/tests/did/src/lib.rs new file mode 100644 index 00000000..b7d87569 --- /dev/null +++ b/ic-stable-structures/tests/did/src/lib.rs @@ -0,0 +1,36 @@ +use candid::{CandidType, Decode, Deserialize, Encode}; +use ic_stable_structures::{BoundedStorable, ChunkSize, SlicedStorable, Storable}; + +pub fn encode(item: &impl CandidType) -> Vec { + Encode!(item).expect("failed to encode item to candid") +} + +pub fn decode<'a, T: CandidType + Deserialize<'a>>(bytes: &'a [u8]) -> T { + Decode!(bytes, T).expect("failed to decode item from candid") +} + +#[derive(Debug, Default, Clone, Copy, CandidType, Deserialize)] +pub struct Transaction { + pub from: u8, + pub to: u8, + pub value: u8, +} + +impl Storable for Transaction { + fn to_bytes(&self) -> std::borrow::Cow<[u8]> { + encode(self).into() + } + + fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { + decode(&bytes) + } +} + +impl SlicedStorable for Transaction { + const CHUNK_SIZE: ChunkSize = 8; +} + +impl BoundedStorable for Transaction { + const MAX_SIZE: u32 = 32; + const IS_FIXED_SIZE: bool = false; +} diff --git a/ic-stable-structures/tests/dummy_canister/.gitignore b/ic-stable-structures/tests/dummy_canister/.gitignore new file mode 100644 index 00000000..e985e881 --- /dev/null +++ b/ic-stable-structures/tests/dummy_canister/.gitignore @@ -0,0 +1 @@ +!dummy_canister.did diff --git a/ic-stable-structures/tests/dummy_canister/Cargo.toml b/ic-stable-structures/tests/dummy_canister/Cargo.toml new file mode 100644 index 00000000..8d19620a --- /dev/null +++ b/ic-stable-structures/tests/dummy_canister/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dummy_canister" +version.workspace = true +edition.workspace = true + +[features] +default = [] +export-api = [] + +[dependencies] +candid = { workspace = true } +did = { path = "../did" } +ic-canister = { path = "../../../ic-canister/ic-canister" } +ic-cdk = { workspace = true } +ic-exports = { path = "../../../ic-exports" } +ic-stable-structures = { path = "../../../ic-stable-structures" } +serde = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/ic-stable-structures/tests/dummy_canister/dummy_canister.did b/ic-stable-structures/tests/dummy_canister/dummy_canister.did new file mode 100644 index 00000000..adb81ff8 --- /dev/null +++ b/ic-stable-structures/tests/dummy_canister/dummy_canister.did @@ -0,0 +1,15 @@ +type Transaction = record { to : nat8; value : nat8; from : nat8 }; +service : () -> { + get_tx_from_btreemap : (nat64) -> (opt Transaction) query; + get_tx_from_cell : () -> (Transaction) query; + get_tx_from_log : (nat64) -> (opt Transaction) query; + get_tx_from_map : (nat64) -> (opt Transaction) query; + get_tx_from_multimap : (nat64) -> (opt Transaction) query; + get_tx_from_vec : (nat64) -> (opt Transaction) query; + insert_tx_to_btreemap : (Transaction) -> (nat64); + insert_tx_to_cell : (Transaction) -> (); + insert_tx_to_map : (Transaction) -> (nat64); + insert_tx_to_multimap : (Transaction) -> (nat64); + push_tx_to_log : (Transaction) -> (nat64); + push_tx_to_vec : (Transaction) -> (nat64); +} diff --git a/ic-stable-structures/tests/dummy_canister/src/canister.rs b/ic-stable-structures/tests/dummy_canister/src/canister.rs new file mode 100644 index 00000000..75cff698 --- /dev/null +++ b/ic-stable-structures/tests/dummy_canister/src/canister.rs @@ -0,0 +1,86 @@ +use candid::Principal; +use did::Transaction; +use ic_canister::{generate_idl, init, query, update, Canister, Idl, PreUpdate}; + +use service::Service; + +mod service; + +#[derive(Canister)] +pub struct DummyCanister { + #[id] + id: Principal, +} + +impl PreUpdate for DummyCanister {} + +impl DummyCanister { + #[init] + pub fn init(&self) { + Service::init() + } + + #[query] + pub fn get_tx_from_btreemap(&self, key: u64) -> Option { + Service::get_tx_from_btreemap(key) + } + + #[update] + pub async fn insert_tx_to_btreemap(&self, transaction: Transaction) -> u64 { + Service::insert_tx_to_btreemap(transaction) + } + + #[query] + pub fn get_tx_from_cell(&self) -> Transaction { + Service::get_tx_from_cell() + } + + #[update] + pub async fn insert_tx_to_cell(&self, transaction: Transaction) { + Service::insert_tx_to_cell(transaction) + } + + #[query] + pub fn get_tx_from_log(&self, idx: u64) -> Option { + Service::get_tx_from_log(idx) + } + + #[update] + pub async fn push_tx_to_log(&self, transaction: Transaction) -> u64 { + Service::push_tx_to_log(transaction) + } + + #[query] + pub fn get_tx_from_map(&self, key: u64) -> Option { + Service::get_tx_from_map(key) + } + + #[update] + pub async fn insert_tx_to_map(&self, transaction: Transaction) -> u64 { + Service::insert_tx_to_map(transaction) + } + + #[query] + pub fn get_tx_from_multimap(&self, key: u64) -> Option { + Service::get_tx_from_multimap(key) + } + + #[update] + pub async fn insert_tx_to_multimap(&self, transaction: Transaction) -> u64 { + Service::insert_tx_to_multimap(transaction) + } + + #[query] + pub fn get_tx_from_vec(&self, idx: u64) -> Option { + Service::get_tx_from_vec(idx) + } + + #[update] + pub async fn push_tx_to_vec(&self, transaction: Transaction) -> u64 { + Service::push_tx_to_vec(transaction) + } + + pub fn idl() -> Idl { + generate_idl!() + } +} diff --git a/ic-stable-structures/tests/dummy_canister/src/canister/service.rs b/ic-stable-structures/tests/dummy_canister/src/canister/service.rs new file mode 100644 index 00000000..abf0578d --- /dev/null +++ b/ic-stable-structures/tests/dummy_canister/src/canister/service.rs @@ -0,0 +1,175 @@ +use ic_stable_structures::{ + MemoryId, StableBTreeMap, StableCell, StableLog, StableMultimap, StableUnboundedMap, StableVec, +}; +use std::cell::RefCell; + +use did::Transaction; + +const TX_MAP_MEMORY_ID: MemoryId = MemoryId::new(1); +const TX_VEC_MEMORY_ID: MemoryId = MemoryId::new(2); +const TX_LOG_INDEX_MEMORY_ID: MemoryId = MemoryId::new(3); +const TX_LOG_MEMORY_ID: MemoryId = MemoryId::new(4); +const TX_CELL_MEMORY_ID: MemoryId = MemoryId::new(5); +const TX_BTREEMAP_MEMORY_ID: MemoryId = MemoryId::new(6); +const TX_MULTIMAP_MEMORY_ID: MemoryId = MemoryId::new(7); + +thread_local! { + + static TX_BTREEMAP: RefCell> = { + RefCell::new(StableBTreeMap::new(TX_BTREEMAP_MEMORY_ID)) + }; + + static TX_CELL: RefCell> = { + RefCell::new(StableCell::new(TX_CELL_MEMORY_ID, Transaction::default()).expect("failed to create stable cell")) + }; + + static TX_LOG: RefCell> = { + RefCell::new(StableLog::new(TX_LOG_INDEX_MEMORY_ID, TX_LOG_MEMORY_ID).expect("failed to create stable log")) + }; + + static TX_MAP: RefCell> = { + RefCell::new(StableUnboundedMap::new(TX_MAP_MEMORY_ID)) + }; + + static TX_MULTIMAP: RefCell> = { + RefCell::new(StableMultimap::new(TX_MULTIMAP_MEMORY_ID)) + }; + + static TX_VEC: RefCell> = { + RefCell::new(StableVec::new(TX_VEC_MEMORY_ID).expect("failed to create stable vec")) + }; + + +} + +#[derive(Default)] +pub struct Service; + +impl Service { + pub fn init() { + let should_init_btreemap = TX_BTREEMAP.with(|txs| txs.borrow().len()) == 0; + if should_init_btreemap { + Self::insert_tx_to_btreemap(Transaction { + from: 0, + to: 0, + value: 0, + }); + } + let should_init_map = TX_MAP.with(|txs| txs.borrow().len()) == 0; + if should_init_map { + Self::insert_tx_to_map(Transaction { + from: 0, + to: 0, + value: 0, + }); + } + let should_init_multimap = TX_MULTIMAP.with(|txs| txs.borrow().len()) == 0; + if should_init_multimap { + Self::insert_tx_to_multimap(Transaction { + from: 0, + to: 0, + value: 0, + }); + } + let should_init_vec = TX_VEC.with(|txs| txs.borrow().len()) == 0; + if should_init_vec { + Self::push_tx_to_vec(Transaction { + from: 0, + to: 0, + value: 0, + }); + } + let should_init_log = TX_LOG.with(|txs| txs.borrow().len()) == 0; + if should_init_log { + Self::push_tx_to_log(Transaction { + from: 0, + to: 0, + value: 0, + }); + } + } + + pub fn get_tx_from_btreemap(key: u64) -> Option { + TX_BTREEMAP.with(|tx| tx.borrow().get(&key)) + } + + pub fn insert_tx_to_btreemap(transaction: Transaction) -> u64 { + TX_BTREEMAP.with(|storage| { + let new_key = storage.borrow().len(); + storage.borrow_mut().insert(new_key, transaction); + + new_key + }) + } + + pub fn get_tx_from_cell() -> Transaction { + TX_CELL.with(|tx| tx.borrow().get().clone()) + } + + pub fn insert_tx_to_cell(transaction: Transaction) { + TX_CELL.with(|storage| { + storage + .borrow_mut() + .set(transaction) + .expect("failed to push to cell"); + }) + } + + pub fn get_tx_from_log(idx: u64) -> Option { + TX_LOG.with(|tx| tx.borrow().get(idx)) + } + + pub fn push_tx_to_log(transaction: Transaction) -> u64 { + TX_LOG.with(|storage| { + storage + .borrow_mut() + .append(transaction) + .expect("failed to push to log"); + + storage.borrow().len() + }) + } + + pub fn get_tx_from_map(key: u64) -> Option { + TX_MAP.with(|tx| tx.borrow().get(&key)) + } + + pub fn insert_tx_to_map(transaction: Transaction) -> u64 { + TX_MAP.with(|storage| { + let new_key = storage.borrow().len(); + storage.borrow_mut().insert(&new_key, &transaction); + + new_key + }) + } + + pub fn get_tx_from_multimap(key: u64) -> Option { + TX_MULTIMAP.with(|tx| tx.borrow().get(&key, &(key + 1))) + } + + pub fn insert_tx_to_multimap(transaction: Transaction) -> u64 { + TX_MULTIMAP.with(|storage| { + let new_key = storage.borrow().len() as u64; + storage + .borrow_mut() + .insert(&new_key, &(new_key + 1), &transaction); + + new_key + }) + } + + pub fn get_tx_from_vec(idx: u64) -> Option { + TX_VEC.with(|tx| tx.borrow().get(idx)) + } + + pub fn push_tx_to_vec(transaction: Transaction) -> u64 { + TX_VEC.with(|storage| { + storage + .borrow_mut() + .push(&transaction) + .expect("failed to push to vec"); + + storage.borrow().len() + }) + } +} diff --git a/ic-stable-structures/tests/dummy_canister/src/main.rs b/ic-stable-structures/tests/dummy_canister/src/main.rs new file mode 100644 index 00000000..c557a4ea --- /dev/null +++ b/ic-stable-structures/tests/dummy_canister/src/main.rs @@ -0,0 +1,11 @@ +pub mod canister; + +use canister::DummyCanister; + +fn main() { + let canister_e_idl = DummyCanister::idl(); + let idl = + candid::bindings::candid::compile(&canister_e_idl.env.env, &Some(canister_e_idl.actor)); + + println!("{}", idl); +} diff --git a/ic-stable-structures/tests/integration_test.rs b/ic-stable-structures/tests/integration_test.rs new file mode 100644 index 00000000..deb7fd16 --- /dev/null +++ b/ic-stable-structures/tests/integration_test.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "state-machine")] +mod state_machine; + +mod utils; diff --git a/ic-stable-structures/tests/state_machine/btreemap.rs b/ic-stable-structures/tests/state_machine/btreemap.rs new file mode 100644 index 00000000..8dd1081c --- /dev/null +++ b/ic-stable-structures/tests/state_machine/btreemap.rs @@ -0,0 +1,39 @@ +use super::with_state_machine_context; + +#[test] +fn should_init_tx_btreemap() { + with_state_machine_context(|_, ctx| { + assert!(ctx.get_tx_from_btreemap(0)?.is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_push_tx_to_btreemap() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_btreemap(1, 1, 10)?; + + assert!(ctx.get_tx_from_btreemap(1).unwrap().is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_persist_btreemap_tx_after_upgrade() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_btreemap(1, 1, 10)?; + + assert!(ctx.get_tx_from_btreemap(1)?.is_some()); + + super::upgrade_dummy_canister(ctx)?; + + assert!(ctx.get_tx_from_btreemap(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} diff --git a/ic-stable-structures/tests/state_machine/cell.rs b/ic-stable-structures/tests/state_machine/cell.rs new file mode 100644 index 00000000..96a5b8bd --- /dev/null +++ b/ic-stable-structures/tests/state_machine/cell.rs @@ -0,0 +1,39 @@ +use super::with_state_machine_context; + +#[test] +fn should_init_tx_cell() { + with_state_machine_context(|_, ctx| { + assert_eq!(ctx.get_tx_from_cell()?.from, 0); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_push_tx_to_cell() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_cell(1, 1, 10)?; + + assert_eq!(ctx.get_tx_from_cell()?.from, 1); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_persist_cell_tx_after_upgrade() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_cell(1, 1, 10)?; + + assert_eq!(ctx.get_tx_from_cell()?.from, 1); + + super::upgrade_dummy_canister(ctx)?; + + assert_eq!(ctx.get_tx_from_cell()?.from, 1); + + Ok(()) + }) + .unwrap(); +} diff --git a/ic-stable-structures/tests/state_machine/log.rs b/ic-stable-structures/tests/state_machine/log.rs new file mode 100644 index 00000000..6e8ddca6 --- /dev/null +++ b/ic-stable-structures/tests/state_machine/log.rs @@ -0,0 +1,40 @@ +use super::with_state_machine_context; + +#[test] +fn should_init_tx_log() { + with_state_machine_context(|_, ctx| { + let res = ctx.get_tx_from_log(0)?; + assert!(res.is_some()); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_push_tx_to_log() { + with_state_machine_context(|_, ctx| { + ctx.push_tx_to_log(1, 1, 10)?; + + assert!(ctx.get_tx_from_log(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_persist_log_tx_after_upgrade() { + with_state_machine_context(|_, ctx| { + ctx.push_tx_to_log(1, 1, 10)?; + + assert!(ctx.get_tx_from_log(1)?.is_some()); + + super::upgrade_dummy_canister(ctx)?; + + assert!(ctx.get_tx_from_log(0)?.is_some()); + assert!(ctx.get_tx_from_log(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} diff --git a/ic-stable-structures/tests/state_machine/map.rs b/ic-stable-structures/tests/state_machine/map.rs new file mode 100644 index 00000000..e2bef4b2 --- /dev/null +++ b/ic-stable-structures/tests/state_machine/map.rs @@ -0,0 +1,40 @@ +use super::with_state_machine_context; + +#[test] +fn should_init_tx_map() { + with_state_machine_context(|_, ctx| { + assert!(ctx.get_tx_from_map(0)?.is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_push_tx_to_map() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_map(1, 1, 10)?; + + assert!(ctx.get_tx_from_map(1).unwrap().is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_persist_map_tx_after_upgrade() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_map(1, 1, 10)?; + + assert!(ctx.get_tx_from_map(1)?.is_some()); + + super::upgrade_dummy_canister(ctx)?; + + assert!(ctx.get_tx_from_map(0)?.is_some()); + assert!(ctx.get_tx_from_map(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} diff --git a/ic-stable-structures/tests/state_machine/mod.rs b/ic-stable-structures/tests/state_machine/mod.rs new file mode 100644 index 00000000..7466d5a8 --- /dev/null +++ b/ic-stable-structures/tests/state_machine/mod.rs @@ -0,0 +1,258 @@ +use std::sync::Mutex; + +use anyhow::Result; +use candid::{Decode, Encode}; +use did::Transaction; +use ic_exports::ic_kit::ic; +use ic_exports::ic_kit::inject; +use ic_exports::ic_kit::mock_principals::alice; +use ic_exports::ic_state_machine_tests::{CanisterId, Cycles, StateMachine}; +use once_cell::sync::Lazy; + +use crate::utils::wasm::get_dummy_canister_bytecode; + +mod btreemap; +mod cell; +mod log; +mod map; +mod multimap; +mod vec; + +#[derive(Debug)] +pub struct StateMachineTestContext { + pub env: StateMachine, + pub dummy_canister: CanisterId, +} + +impl StateMachineTestContext { + pub fn get_tx_from_btreemap(&self, key: u64) -> Result> { + let args = Encode!(&key).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "get_tx_from_btreemap", + args, + )?; + + let tx = Decode!(&res.bytes(), Option)?; + + Ok(tx) + } + + pub fn insert_tx_to_btreemap(&self, from: u8, to: u8, value: u8) -> Result { + let args = Encode!(&Transaction { from, to, value }).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "insert_tx_to_btreemap", + args, + )?; + + let key = Decode!(&res.bytes(), u64)?; + + Ok(key) + } + + pub fn get_tx_from_cell(&self) -> Result { + let args = Encode!(&()).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "get_tx_from_cell", + args, + )?; + + let tx = Decode!(&res.bytes(), Transaction)?; + + Ok(tx) + } + + pub fn insert_tx_to_cell(&self, from: u8, to: u8, value: u8) -> Result<()> { + let args = Encode!(&Transaction { from, to, value }).unwrap(); + self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "insert_tx_to_cell", + args, + )?; + + Ok(()) + } + + pub fn get_tx_from_map(&self, key: u64) -> Result> { + let args = Encode!(&key).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "get_tx_from_map", + args, + )?; + + let tx = Decode!(&res.bytes(), Option)?; + + Ok(tx) + } + + pub fn insert_tx_to_map(&self, from: u8, to: u8, value: u8) -> Result { + let args = Encode!(&Transaction { from, to, value }).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "insert_tx_to_map", + args, + )?; + + let key = Decode!(&res.bytes(), u64)?; + + Ok(key) + } + + pub fn get_tx_from_multimap(&self, key: u64) -> Result> { + let args = Encode!(&key).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "get_tx_from_multimap", + args, + )?; + + let tx = Decode!(&res.bytes(), Option)?; + + Ok(tx) + } + + pub fn insert_tx_to_multimap(&self, from: u8, to: u8, value: u8) -> Result { + let args = Encode!(&Transaction { from, to, value }).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "insert_tx_to_multimap", + args, + )?; + + let key = Decode!(&res.bytes(), u64)?; + + Ok(key) + } + + pub fn get_tx_from_vec(&self, index: u64) -> Result> { + let args = Encode!(&index).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "get_tx_from_vec", + args, + )?; + + let tx = Decode!(&res.bytes(), Option)?; + + Ok(tx) + } + + pub fn push_tx_to_vec(&self, from: u8, to: u8, value: u8) -> Result { + let args = Encode!(&Transaction { from, to, value }).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "push_tx_to_vec", + args, + )?; + + let key = Decode!(&res.bytes(), u64)?; + + Ok(key) + } + + pub fn get_tx_from_log(&self, index: u64) -> Result> { + let args = Encode!(&index).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "get_tx_from_log", + args, + )?; + + let tx = Decode!(&res.bytes(), Option)?; + + Ok(tx) + } + + pub fn push_tx_to_log(&self, from: u8, to: u8, value: u8) -> Result { + let args = Encode!(&Transaction { from, to, value }).unwrap(); + let res = self.env.execute_ingress_as( + ic::caller().into(), + self.dummy_canister, + "push_tx_to_log", + args, + )?; + + let key = Decode!(&res.bytes(), u64)?; + + Ok(key) + } +} + +pub fn with_state_machine_context<'a, F>(f: F) -> Result<()> +where + F: FnOnce(&'a mut ic_exports::ic_kit::MockContext, &StateMachineTestContext) -> Result<()>, +{ + ic_exports::ic_kit::MockContext::new() + .with_caller(alice()) + .with_id(alice()) + .inject(); + + static TEST_CONTEXT: Lazy> = Lazy::new(|| { + let env = StateMachine::new(); + let dummy_canister = deploy_dummy_canister(&env).unwrap(); + StateMachineTestContext { + env, + dummy_canister, + } + .into() + }); + let test_ctx = TEST_CONTEXT.lock().unwrap(); + let ctx = inject::get_context(); + + f(ctx, &test_ctx)?; + + reinstall_dummy_canister(&test_ctx)?; + + Ok(()) +} + +fn deploy_dummy_canister(env: &StateMachine) -> Result { + let dummy_wasm = get_dummy_canister_bytecode(); + eprintln!("Creating dummy canister"); + + let args = Encode!(&())?; + let canister = env.install_canister_with_cycles( + dummy_wasm.to_vec(), + args, + None, + Cycles::new(10_u128.pow(12)), + )?; + + Ok(canister) +} + +pub fn reinstall_dummy_canister(ctx: &StateMachineTestContext) -> Result<()> { + let args = Encode!(&())?; + + let dummy_wasm = get_dummy_canister_bytecode(); + + ctx.env + .reinstall_canister(ctx.dummy_canister, dummy_wasm, args)?; + + Ok(()) +} + +pub fn upgrade_dummy_canister(ctx: &StateMachineTestContext) -> Result<()> { + let args = Encode!(&())?; + + let dummy_wasm = get_dummy_canister_bytecode(); + + ctx.env + .upgrade_canister(ctx.dummy_canister, dummy_wasm, args)?; + + Ok(()) +} diff --git a/ic-stable-structures/tests/state_machine/multimap.rs b/ic-stable-structures/tests/state_machine/multimap.rs new file mode 100644 index 00000000..3e3eaa33 --- /dev/null +++ b/ic-stable-structures/tests/state_machine/multimap.rs @@ -0,0 +1,40 @@ +use super::with_state_machine_context; + +#[test] +fn should_init_tx_multimap() { + with_state_machine_context(|_, ctx| { + assert!(ctx.get_tx_from_multimap(0)?.is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_push_tx_to_multimap() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_multimap(1, 1, 10)?; + + assert!(ctx.get_tx_from_multimap(1).unwrap().is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_persist_multimap_tx_after_upgrade() { + with_state_machine_context(|_, ctx| { + ctx.insert_tx_to_multimap(1, 1, 10)?; + + assert!(ctx.get_tx_from_multimap(1)?.is_some()); + + super::upgrade_dummy_canister(ctx)?; + + assert!(ctx.get_tx_from_multimap(0)?.is_some()); + assert!(ctx.get_tx_from_multimap(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} diff --git a/ic-stable-structures/tests/state_machine/vec.rs b/ic-stable-structures/tests/state_machine/vec.rs new file mode 100644 index 00000000..79677c20 --- /dev/null +++ b/ic-stable-structures/tests/state_machine/vec.rs @@ -0,0 +1,40 @@ +use super::with_state_machine_context; + +#[test] +fn should_init_tx_vec() { + with_state_machine_context(|_, ctx| { + let res = ctx.get_tx_from_vec(0)?; + assert!(res.is_some()); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_push_tx_to_vec() { + with_state_machine_context(|_, ctx| { + ctx.push_tx_to_vec(1, 1, 10)?; + + assert!(ctx.get_tx_from_vec(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} + +#[test] +fn should_persist_vec_tx_after_upgrade() { + with_state_machine_context(|_, ctx| { + ctx.push_tx_to_vec(1, 1, 10)?; + + assert!(ctx.get_tx_from_vec(1)?.is_some()); + + super::upgrade_dummy_canister(ctx)?; + + assert!(ctx.get_tx_from_vec(0)?.is_some()); + assert!(ctx.get_tx_from_vec(1)?.is_some()); + + Ok(()) + }) + .unwrap(); +} diff --git a/ic-stable-structures/tests/utils/mod.rs b/ic-stable-structures/tests/utils/mod.rs new file mode 100644 index 00000000..d9d6e667 --- /dev/null +++ b/ic-stable-structures/tests/utils/mod.rs @@ -0,0 +1,13 @@ +use std::path::{Path, PathBuf}; + +pub mod wasm; + +/// Returns the Path to the workspace root dir +pub fn get_workspace_root_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() +} diff --git a/ic-stable-structures/tests/utils/wasm.rs b/ic-stable-structures/tests/utils/wasm.rs new file mode 100644 index 00000000..97bd72db --- /dev/null +++ b/ic-stable-structures/tests/utils/wasm.rs @@ -0,0 +1,56 @@ +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use once_cell::sync::OnceCell; + +use super::get_workspace_root_dir; + +/// Returns the bytecode of the evmc canister +pub fn get_dummy_canister_bytecode() -> Vec { + static CANISTER_BYTECODE: OnceCell> = OnceCell::new(); + CANISTER_BYTECODE + .get_or_init(|| load_wasm_bytecode_or_panic("dummy_canister.wasm")) + .to_owned() +} + +fn load_wasm_bytecode_or_panic(wasm_name: &str) -> Vec { + let path = get_path_to_wasm(wasm_name); + + let mut f = File::open(path).expect("File does not exists"); + + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer) + .expect("Could not read file content"); + + buffer +} + +fn get_path_to_wasm(wasm_name: &str) -> PathBuf { + if let Ok(dir_path) = std::env::var("WASMS_DIR") { + let wasm_path = Path::new(&dir_path).join(wasm_name); + + if wasm_path.as_path().exists() { + return wasm_path; + } + } else { + const ARTIFACT_PATH: &str = ".artifact"; + // Get to the root of the project + let root_dir = get_workspace_root_dir(); + let wasm_path = root_dir.join(ARTIFACT_PATH).join(wasm_name); + if wasm_path.as_path().exists() { + return wasm_path; + } + } + + if let Ok(dir_path) = std::env::var("DFX_WASMS_DIR") { + let wasm_path = Path::new(&dir_path).join(wasm_name); + if wasm_path.as_path().exists() { + return wasm_path; + } + } + + panic!( + "File {wasm_name} was not found in dirs provided by ENV variables WASMS_DIR or DFX_WASMS_DIR or in the '.artifact' folder" + ); +} diff --git a/scripts/build.sh b/scripts/build.sh index 02075f5d..23b1a6e9 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,13 +4,16 @@ cargo run -p canister-a --features export-api > ic-canister/tests/canister-a/can cargo run -p canister-b --features export-api > ic-canister/tests/canister-b/canister-b.did cargo run -p canister-c --features export-api > ic-canister/tests/canister-c/canister-c.did cargo run -p canister-d --features export-api > ic-canister/tests/canister-d/canister-d.did +cargo run -p dummy_canister --features export-api > ic-stable-structures/tests/dummy_canister/dummy_canister.did cargo build -p canister-a --lib --target wasm32-unknown-unknown --features export-api --release cargo build -p canister-b --lib --target wasm32-unknown-unknown --features export-api --release cargo build -p canister-c --lib --target wasm32-unknown-unknown --features export-api --release cargo build -p canister-d --lib --target wasm32-unknown-unknown --features export-api --release +cargo build -p dummy_canister --target wasm32-unknown-unknown --features export-api --release ic-cdk-optimizer target/wasm32-unknown-unknown/release/canister_a.wasm -o target/wasm32-unknown-unknown/release/canister-a.wasm ic-cdk-optimizer target/wasm32-unknown-unknown/release/canister_b.wasm -o target/wasm32-unknown-unknown/release/canister-b.wasm ic-cdk-optimizer target/wasm32-unknown-unknown/release/canister_c.wasm -o target/wasm32-unknown-unknown/release/canister-c.wasm ic-cdk-optimizer target/wasm32-unknown-unknown/release/canister_d.wasm -o target/wasm32-unknown-unknown/release/canister-d.wasm + diff --git a/scripts/build_dummy_canister.sh b/scripts/build_dummy_canister.sh new file mode 100755 index 00000000..c42915ec --- /dev/null +++ b/scripts/build_dummy_canister.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +cargo run -p dummy_canister --features export-api > ic-stable-structures/tests/dummy_canister/dummy_canister.did +cargo build -p dummy_canister --target wasm32-unknown-unknown --features export-api --release +ic-wasm target/wasm32-unknown-unknown/release/dummy_canister.wasm -o target/wasm32-unknown-unknown/release/dummy_canister.wasm shrink +gzip -k target/wasm32-unknown-unknown/release/dummy_canister.wasm --force