diff --git a/.config/forest.dic b/.config/forest.dic index 4e3f716ababd..7afa41e9c47e 100644 --- a/.config/forest.dic +++ b/.config/forest.dic @@ -1,4 +1,4 @@ -274 +276 Algorand/M API's API/SM @@ -147,13 +147,14 @@ MD5 MDBX MDNS MDX -mempool +mempool/M Merkle MetaMask MiB middleware migrator/S milliGas +mpool multiaddr/SM multiaddress multiaddresses @@ -163,6 +164,7 @@ mutex namespace/S Neo4j nextest +nonces NVMe NVXX onwards diff --git a/.github/workflows/forest.yml b/.github/workflows/forest.yml index 891d4e9a3268..24b86aba80ab 100644 --- a/.github/workflows/forest.yml +++ b/.github/workflows/forest.yml @@ -15,7 +15,7 @@ on: - main # This needs to be declared explicitly so that the RPC checks job is actually # run when PR is labeled. - types: [opened, synchronize, reopened, labeled, unlabeled] + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] paths-ignore: - "docs/**" - ".github/workflows/docs-*.yml" @@ -226,6 +226,7 @@ jobs: run: ./scripts/tests/calibnet_migration_regression_tests.sh timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} calibnet-wallet-check: + if: github.event.pull_request.draft == false concurrency: group: calibnet-wallet-tests cancel-in-progress: false diff --git a/Cargo.lock b/Cargo.lock index bc0b57429e0d..23558bde6b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2535,6 +2535,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + [[package]] name = "etcetera" version = "0.11.0" @@ -3336,6 +3342,7 @@ dependencies = [ "libm", "libp2p", "libp2p-swarm-test", + "libtest-mimic", "md-5", "memmap2 0.9.10", "multiaddr", @@ -5976,6 +5983,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" diff --git a/Cargo.toml b/Cargo.toml index 515e95dab453..b76ddc3ae500 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,7 @@ zstd = "0.13" # optional dependencies console-subscriber = { version = "0.5", features = ["parking_lot"], optional = true } +libtest-mimic = "0.8" sqlx = { version = "0.9", default-features = false, features = ["sqlite", "runtime-tokio", "macros"], optional = true } tikv-jemallocator = { version = "0.7", optional = true } tracing-loki = { version = "0.2", default-features = false, features = ["compat-0-2-1", "rustls"], optional = true } @@ -351,7 +352,6 @@ cargo-test = [] # group of tests that is recommended to run with `cargo test` in doctest-private = [] # see lib.rs::doctest_private benchmark-private = ["dep:criterion"] # see lib.rs::benchmark_private interop-tests-private = [] # see lib.rs::interop_tests_private -calibnet-integration = [] # see tests/calibnet_*.rs (wallet, mpool_tools, etc.) jemalloc-profiling = [ "jemalloc", "tikv-jemallocator?/profiling", @@ -384,16 +384,6 @@ name = "tipset-validation" harness = false required-features = ["benchmark-private"] -[[test]] -name = "mpool_tools" -path = "tests/calibnet_mpool_tools.rs" -required-features = ["calibnet-integration"] - -[[test]] -name = "wallet" -path = "tests/calibnet_wallet.rs" -required-features = ["calibnet-integration"] - [package.metadata.docs.rs] # See https://docs.rs/about/metadata rustdoc-args = ["--document-private-items"] diff --git a/mise.toml b/mise.toml index 357229bf2075..ce9cbb99d816 100644 --- a/mise.toml +++ b/mise.toml @@ -221,8 +221,8 @@ run = ''' set -euo pipefail source ./scripts/tests/harness.sh forest_wallet_init "${usage_preloaded_key?}" -cargo test --profile quick-test --features calibnet-integration --test mpool_tools -- --nocapture -cargo test --profile quick-test --features calibnet-integration --test wallet -- --nocapture +forest-dev tests calibnet mpool +forest-dev tests calibnet wallet ''' [tasks."codecov:nextest"] diff --git a/src/dev/subcommands/mod.rs b/src/dev/subcommands/mod.rs index caf49d19477b..2cf3caf15f53 100644 --- a/src/dev/subcommands/mod.rs +++ b/src/dev/subcommands/mod.rs @@ -5,6 +5,7 @@ mod archive_missing_cmd; mod export_state_tree_cmd; mod export_tipset_lookup_cmd; mod state_cmd; +mod tests_cmd; mod update_checkpoints_cmd; use crate::cli_shared::cli::HELP_MESSAGE; @@ -53,6 +54,8 @@ pub enum Subcommand { ArchiveMissing(archive_missing_cmd::ArchiveMissingCommand), ExportTipsetLookup(export_tipset_lookup_cmd::ExportTipsetLookupCommand), ExportStateTree(export_state_tree_cmd::ExportStateTreeCommand), + #[command(subcommand)] + Tests(tests_cmd::TestsCommand), } impl Subcommand { @@ -64,6 +67,7 @@ impl Subcommand { Self::ArchiveMissing(cmd) => cmd.run().await, Self::ExportTipsetLookup(cmd) => cmd.run().await, Self::ExportStateTree(cmd) => cmd.run().await, + Self::Tests(cmd) => cmd.run().await, } } } diff --git a/src/dev/subcommands/tests_cmd.rs b/src/dev/subcommands/tests_cmd.rs new file mode 100644 index 000000000000..8314764cf8c8 --- /dev/null +++ b/src/dev/subcommands/tests_cmd.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +mod calibnet; + +/// Integration tests +#[derive(Debug, clap::Subcommand)] +pub enum TestsCommand { + #[command(subcommand)] + Calibnet(calibnet::CalibnetTestsCommand), +} + +impl TestsCommand { + pub async fn run(self) -> anyhow::Result<()> { + match self { + Self::Calibnet(cmd) => cmd.run().await, + } + } +} diff --git a/src/dev/subcommands/tests_cmd/calibnet.rs b/src/dev/subcommands/tests_cmd/calibnet.rs new file mode 100644 index 000000000000..83dd5a7c8b43 --- /dev/null +++ b/src/dev/subcommands/tests_cmd/calibnet.rs @@ -0,0 +1,22 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +mod helpers; +mod mpool; +mod wallet; + +/// Calibnet integration tests +#[derive(Debug, clap::Subcommand)] +pub enum CalibnetTestsCommand { + Wallet(wallet::CalibnetWalletTestCommand), + Mpool(mpool::CalibnetMpoolTestCommand), +} + +impl CalibnetTestsCommand { + pub async fn run(self) -> anyhow::Result<()> { + match self { + Self::Wallet(cmd) => cmd.run().await, + Self::Mpool(cmd) => cmd.run().await, + } + } +} diff --git a/tests/common/calibnet_wallet_helpers.rs b/src/dev/subcommands/tests_cmd/calibnet/helpers.rs similarity index 98% rename from tests/common/calibnet_wallet_helpers.rs rename to src/dev/subcommands/tests_cmd/calibnet/helpers.rs index b071153e8450..26025dfc2687 100644 --- a/tests/common/calibnet_wallet_helpers.rs +++ b/src/dev/subcommands/tests_cmd/calibnet/helpers.rs @@ -378,3 +378,10 @@ pub async fn filecoin_to_eth(address: &str) -> anyhow::Result { .map(str::to_owned) .with_context(|| format!("expected string ETH address, got {result}")) } + +pub fn block_on(future: F) -> F::Output { + tokio::task::block_in_place(|| { + let rt = tokio::runtime::Handle::current(); + rt.block_on(future) + }) +} diff --git a/tests/calibnet_mpool_tools.rs b/src/dev/subcommands/tests_cmd/calibnet/mpool.rs similarity index 68% rename from tests/calibnet_mpool_tools.rs rename to src/dev/subcommands/tests_cmd/calibnet/mpool.rs index 2c54f4101676..40a7a3cd1f01 100644 --- a/tests/calibnet_mpool_tools.rs +++ b/src/dev/subcommands/tests_cmd/calibnet/mpool.rs @@ -6,14 +6,36 @@ //! Run via [`calibnet_wallet_mpool`] before [`calibnet_wallet`]; see `mise test:wallet`. //! Each test assumes the same environment as [`calibnet_wallet`]. -#[path = "common/calibnet_wallet_helpers.rs"] -mod helpers; +use super::helpers::*; +use libtest_mimic::{Arguments, Trial}; -use helpers::*; -use serial_test::serial; +/// Calibnet mpool integration tests +#[derive(Debug, clap::Args)] +pub struct CalibnetMpoolTestCommand {} + +impl CalibnetMpoolTestCommand { + pub async fn run(self) -> anyhow::Result<()> { + let args = Arguments { + test_threads: Some(1), + ..Default::default() + }; + libtest_mimic::run(&args, tests()).exit(); + } +} + +fn tests() -> Vec { + vec![ + Trial::test("mpool_nonce_fix_auto_unblocks_pending", || { + block_on(mpool_nonce_fix_auto_unblocks_pending()); + Ok(()) + }), + Trial::test("mpool_replace_auto_unblocks_pending", || { + block_on(mpool_replace_auto_unblocks_pending()); + Ok(()) + }), + ] +} -#[tokio::test] -#[serial] async fn mpool_nonce_fix_auto_unblocks_pending() { let addr = FOREST_TEST_PRELOADED_ADDRESS.as_str(); let nonce = mpool_nonce(addr).unwrap(); @@ -40,8 +62,6 @@ async fn mpool_nonce_fix_auto_unblocks_pending() { ); } -#[tokio::test] -#[serial] async fn mpool_replace_auto_unblocks_pending() { let addr = FOREST_TEST_PRELOADED_ADDRESS.as_str(); let nonce = mpool_nonce(addr).unwrap(); diff --git a/tests/calibnet_wallet.rs b/src/dev/subcommands/tests_cmd/calibnet/wallet.rs similarity index 62% rename from tests/calibnet_wallet.rs rename to src/dev/subcommands/tests_cmd/calibnet/wallet.rs index 27be9ce6861e..d999fdddb3ce 100644 --- a/tests/calibnet_wallet.rs +++ b/src/dev/subcommands/tests_cmd/calibnet/wallet.rs @@ -1,24 +1,77 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! Calibnet wallet integration tests. Each test assumes: -//! - `forest-wallet` and `forest-cli` are on `PATH`, -//! - a Forest daemon is running and synced to calibnet, -//! - [`FOREST_TEST_PRELOADED_ADDRESS`] is funded and imported into both backends (env var of the same name; see `forest_wallet_init`), -//! - `FULLNODE_API_INFO` is exported. +use super::helpers::*; +use libtest_mimic::{Arguments, Trial}; -#[path = "common/calibnet_wallet_helpers.rs"] -mod helpers; +/// Calibnet wallet integration tests +#[derive(Debug, clap::Args)] +pub struct CalibnetWalletTestCommand {} -use helpers::*; -use rstest::rstest; -use serde_json::json; +impl CalibnetWalletTestCommand { + pub async fn run(self) -> anyhow::Result<()> { + let args = Arguments { + test_threads: Some(1), + ..Default::default() + }; + libtest_mimic::run(&args, tests()).exit(); + } +} + +fn tests() -> Vec { + vec![ + Trial::test("export_import_roundtrip_local", || { + block_on(export_import_roundtrip(Backend::Local)); + Ok(()) + }), + Trial::test("export_import_roundtrip_remote", || { + block_on(export_import_roundtrip(Backend::Remote)); + Ok(()) + }), + Trial::test("market_add_balance_message_on_chain", || { + block_on(market_add_balance_message_on_chain()); + Ok(()) + }), + Trial::test("send_to_filecoin_address_local", || { + block_on(send_to_filecoin_address(Backend::Local)); + Ok(()) + }), + Trial::test("send_to_filecoin_address_remote", || { + block_on(send_to_filecoin_address(Backend::Remote)); + Ok(()) + }), + Trial::test("send_to_eth_equivalent_local", || { + block_on(send_to_eth_equivalent(Backend::Local)); + Ok(()) + }), + Trial::test("send_to_eth_equivalent_remote", || { + block_on(send_to_eth_equivalent(Backend::Remote)); + Ok(()) + }), + Trial::test("wallet_delete_local", || { + block_on(wallet_delete(Backend::Local)); + Ok(()) + }), + Trial::test("wallet_delete_remote", || { + block_on(wallet_delete(Backend::Remote)); + Ok(()) + }), + Trial::test("delegated_send_local", || { + block_on(delegated_send(Backend::Local)); + Ok(()) + }), + Trial::test("delegated_send_remote", || { + block_on(delegated_send(Backend::Remote)); + Ok(()) + }), + Trial::test("delegated_remote_send", || { + block_on(delegated_remote_send()); + Ok(()) + }), + ] +} -#[rstest] -#[case::local(Backend::Local)] -#[case::remote(Backend::Remote)] -#[tokio::test] -async fn export_import_roundtrip(#[case] backend: Backend) { +async fn export_import_roundtrip(backend: Backend) { let addr = wallet(backend, &["new"]).unwrap(); let exported = export_to_temp_file(&addr, backend).unwrap(); let path = exported @@ -38,12 +91,11 @@ async fn export_import_roundtrip(#[case] backend: Backend) { ); } -#[tokio::test] async fn market_add_balance_message_on_chain() { const ATTO_FIL: &str = "23"; let result = rpc_call( "Filecoin.MarketAddBalance", - json!([ + serde_json::json!([ FOREST_TEST_PRELOADED_ADDRESS.as_str(), FOREST_TEST_PRELOADED_ADDRESS.as_str(), ATTO_FIL, @@ -55,11 +107,7 @@ async fn market_add_balance_message_on_chain() { poll_until_state_search_msg(&msg_cid).await.unwrap(); } -#[rstest] -#[case::local(Backend::Local)] -#[case::remote(Backend::Remote)] -#[tokio::test] -async fn send_to_filecoin_address(#[case] backend: Backend) { +async fn send_to_filecoin_address(backend: Backend) { let target = wallet(backend, &["new"]).unwrap(); let msg = send_from(&FOREST_TEST_PRELOADED_ADDRESS, &target, FIL_AMT, backend).unwrap(); eprintln!("send to {target} ({}) msg: {msg}", backend.label()); @@ -67,11 +115,7 @@ async fn send_to_filecoin_address(#[case] backend: Backend) { eprintln!("{target} funded balance: {funded}"); } -#[rstest] -#[case::local(Backend::Local)] -#[case::remote(Backend::Remote)] -#[tokio::test] -async fn send_to_eth_equivalent(#[case] backend: Backend) { +async fn send_to_eth_equivalent(backend: Backend) { let target = wallet(backend, &["new"]).unwrap(); let initial_msg = send_from(&FOREST_TEST_PRELOADED_ADDRESS, &target, FIL_AMT, backend).unwrap(); eprintln!( @@ -93,11 +137,7 @@ async fn send_to_eth_equivalent(#[case] backend: Backend) { ); } -#[rstest] -#[case::local(Backend::Local)] -#[case::remote(Backend::Remote)] -#[tokio::test] -async fn wallet_delete(#[case] backend: Backend) { +async fn wallet_delete(backend: Backend) { let addr = wallet(backend, &["new"]).unwrap(); let deleted = wallet(backend, &["delete", &addr]).unwrap(); eprintln!("delete output ({}): {deleted}", backend.label()); @@ -108,11 +148,7 @@ async fn wallet_delete(#[case] backend: Backend) { ); } -#[rstest] -#[case::local(Backend::Local)] -#[case::remote(Backend::Remote)] -#[tokio::test] -async fn delegated_send(#[case] target_backend: Backend) { +async fn delegated_send(target_backend: Backend) { let funded = funded_delegated_addr().await; let target = wallet(target_backend, &["new", "delegated"]).unwrap(); // Baseline `FIL_ZERO` ⇒ first credit; otherwise expect a balance delta. @@ -135,7 +171,6 @@ async fn delegated_send(#[case] target_backend: Backend) { ); } -#[tokio::test] async fn delegated_remote_send() { let funded = funded_delegated_addr().await; let target = wallet(Backend::Remote, &["new", "delegated"]).unwrap();