diff --git a/mvr-cli/Cargo.lock b/mvr-cli/Cargo.lock index 9d148a7..c1ee069 100644 --- a/mvr-cli/Cargo.lock +++ b/mvr-cli/Cargo.lock @@ -1605,6 +1605,12 @@ dependencies = [ "syn 2.0.70", ] +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + [[package]] name = "doc-comment" version = "0.3.3" @@ -1783,6 +1789,16 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "expect-test" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "eyre" version = "0.6.12" @@ -3486,6 +3502,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "expect-test", "reqwest 0.12.7", "serde", "serde_json", diff --git a/mvr-cli/Cargo.toml b/mvr-cli/Cargo.toml index d28aca7..23556b0 100644 --- a/mvr-cli/Cargo.toml +++ b/mvr-cli/Cargo.toml @@ -29,3 +29,6 @@ tokio = "1.38.0" url = "2.5.2" toml_edit = "0.22.21" toml = "0.8.19" + +[dev-dependencies] +expect-test = "1.1" \ No newline at end of file diff --git a/mvr-cli/src/lib.rs b/mvr-cli/src/lib.rs index ec1fdf9..713147a 100644 --- a/mvr-cli/src/lib.rs +++ b/mvr-cli/src/lib.rs @@ -53,17 +53,17 @@ pub enum PackageInfoNetwork { } #[derive(Debug)] -struct PackageInfo { - upgrade_cap_id: ObjectID, - package_address: ObjectID, - git_versioning: HashMap, +pub struct PackageInfo { + pub upgrade_cap_id: ObjectID, + pub package_address: ObjectID, + pub git_versioning: HashMap, } #[derive(Debug)] -struct GitInfo { - repository: String, - tag: String, - path: String, +pub struct GitInfo { + pub repository: String, + pub tag: String, + pub path: String, } impl fmt::Display for PackageInfoNetwork { @@ -259,7 +259,7 @@ async fn resolve_on_chain_package_info( Ok(resolved_packages) } -async fn check_address_consistency( +pub async fn check_address_consistency( resolved_packages: &HashMap, network: &PackageInfoNetwork, fetched_files: &HashMap, @@ -287,7 +287,7 @@ async fn check_address_consistency( anyhow::anyhow!("Failed to retrieve original package address at version 1") })? } else { - package_info.package_address.to_string() + package_info.package_address }; let git_info = package_info.git_versioning.get(&version).ok_or_else(|| { @@ -299,7 +299,7 @@ async fn check_address_consistency( let (move_toml_path, move_lock_path) = fetched_files .get(name_with_version) - .ok_or_else(|| anyhow!("Failed to find fetched files for {}", name_with_version))?; + .ok_or_else(|| anyhow!("Failed to find fetched files `Move.toml` and `Move.lock when checking address consistency for {}", name_with_version))?; let move_toml_content = fs::read_to_string(move_toml_path)?; let move_lock_content = fs::read_to_string(move_lock_path)?; @@ -310,11 +310,35 @@ async fn check_address_consistency( PackageInfoNetwork::Mainnet => MAINNET_CHAIN_ID, PackageInfoNetwork::Testnet => TESTNET_CHAIN_ID, }; + let address = address + .map(|id_str| { + ObjectID::from_hex_literal(&id_str).map_err(|e| { + anyhow!( + "Failed to parse address in [addresses] section of Move.toml: {}", + e + ) + }) + }) + .transpose()?; + let published_at = published_at + .map(|id_str| { + ObjectID::from_hex_literal(&id_str).map_err(|e| { + anyhow!("Failed to parse published-at address of Move.toml: {}", e) + }) + }) + .transpose()?; + // The original-published-id may exist in the Move.lock let original_published_id_in_lock = - get_original_published_id(&move_lock_content, target_chain_id); + get_original_published_id(&move_lock_content, target_chain_id) + .map(|id_str| { + ObjectID::from_hex_literal(&id_str).map_err(|e| { + anyhow!("Failed to parse original-published-id in Move.lock: {}", e) + }) + }) + .transpose()?; - let (original_source_id, provenance): (String, String) = match ( + let (original_source_id, provenance): (ObjectID, String) = match ( original_published_id_in_lock, published_at, address, @@ -325,22 +349,28 @@ async fn check_address_consistency( // to reliably identify it by). // Our best guess is that the published-id refers to the original package (it may not, but // if it doesn't, there is nowhere else to look in this case). - (published_at_id, "published-at in the Move.toml".into()) + ( + published_at_id, + "published-at address in the Move.toml".into(), + ) } (None, Some(published_at_id), Some(address_id)) - if address_id == "0x0" || published_at_id == address_id => + if address_id == ObjectID::ZERO || published_at_id == address_id => { // The [addresses] section has a package name set to "0x0" or the same as the published_at_id. // Our best guess is that the published-id refers to the original package (it may not, but // if it doesn't, there is nowhere else to look in this case). - (published_at_id, "published-at in the Move.toml".into()) + ( + published_at_id, + "published-at address in the Move.toml".into(), + ) } (None, _, Some(address_id)) => { // A published-at ID may or may not exist. In either case, it differs from the // address ID. The address ID that may refer to the original package (e.g., if the // package was upgraded). // Our best guess is that the id in the [addresses] section refers to the original ID. - // It may be "0x0" or the original ID + // It may be "0x0" or the original ID. ( address_id, "address in the [addresses] section of the Move.toml".into(), @@ -361,7 +391,7 @@ async fn check_address_consistency( // Main consistency check: The on-chain package address should correspond to the original ID in the source package. if original_address_on_chain != original_source_id { bail!( - "Mismatch: The original package address for {name} is {original_address_on_chain},\ + "Mismatch: The original package address for {name} on {network} is {original_address_on_chain}, \ but the {provenance} in {name}'s repository was found to be {original_source_id}.\n\ Check the configuration of the package's repository {} in branch {} in subdirectory {}", git_info.repository, @@ -413,13 +443,13 @@ async fn get_published_ids(move_toml_content: &str) -> (Option, Option Some(addr), + match package_with_zero_address { + Some(_) => Some("0x0".into()), None => { let package_name = package_table .get("name") @@ -467,7 +497,7 @@ fn get_original_published_id(move_toml_content: &str, target_chain_id: &str) -> /// Since we want to communicate `foo` (and the URL where it can be found) to `sui move build`, /// we create a dependency graph in the `Move.lock` derived from `foo`'s original lock file /// that contains `foo`. See `insert_root_dependency` for how this works. -async fn build_lock_files( +pub async fn build_lock_files( resolved_packages: &HashMap, fetched_files: &HashMap, ) -> Result> { @@ -493,9 +523,13 @@ async fn build_lock_files( .get(&version) .ok_or_else(|| anyhow!("version {version} does not exist in on-chain PackageInfo"))?; - let (move_toml_path, move_lock_path) = fetched_files - .get(name_with_version) - .ok_or_else(|| anyhow!("Failed to find fetched files for {}", name_with_version))?; + let (move_toml_path, move_lock_path) = + fetched_files.get(name_with_version).ok_or_else(|| { + anyhow!( + "Failed to find fetched files `Move.toml` and `Move.lock` when building package graph for {}", + name_with_version + ) + })?; let move_toml_content = fs::read_to_string(move_toml_path)?; let move_lock_content = fs::read_to_string(move_lock_path)?; let root_name_from_source = parse_source_package_name(&move_toml_content)?; @@ -703,7 +737,7 @@ async fn package_at_version( address: &str, version: u64, network: &PackageInfoNetwork, -) -> Result> { +) -> Result> { let endpoint = match network { PackageInfoNetwork::Mainnet => MAINNET_GQL, PackageInfoNetwork::Testnet => TESTNET_GQL, @@ -733,6 +767,16 @@ async fn package_at_version( let result = body["data"]["package"]["packageAtVersion"]["address"] .as_str() .map(String::from); + let result = result + .map(|id_str| { + ObjectID::from_hex_literal(&id_str).map_err(|e| { + anyhow!( + "Failed to parse package address for packageAtVersion GQL request: {}", + e + ) + }) + }) + .transpose()?; Ok(result) } @@ -777,7 +821,7 @@ fn extract_git_info(dynamic_field_data: &SuiObjectResponse) -> Result { } /// Given a normalized Move Registry package name, split out the version number (if any). -fn parse_package_version(name: &str) -> anyhow::Result<(String, Option)> { +pub fn parse_package_version(name: &str) -> anyhow::Result<(String, Option)> { let parts: Vec<&str> = name.split('/').collect(); match parts.as_slice() { [base_org, base_name] => Ok(( @@ -963,70 +1007,6 @@ fn parse_source_package_name(toml_content: &str) -> Result { Ok(name.to_string()) } -fn _demo_package_move_toml() -> String { - String::from( - r#"[package] -name = "demo" -edition = "2024.beta" - -[dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } - -[addresses] -demo = "0x0" - -[dev-dependencies] - -[dev-addresses] -"#, - ) -} - -fn _demo_package_move_lock() -> String { - String::from( - r#"# @generated by Move, please check-in and do not edit manually. - -[move] -version = 2 -manifest_digest = "72EAB20899F5ADF42787258ACEE32F8531E06BB319B82DFA6866FA4807AAC42E" -deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" -dependencies = [ - { name = "Sui" }, -] - -[[move.package]] -name = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } - -[[move.package]] -name = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } - -dependencies = [ - { name = "MoveStdlib" }, -] - -[move.toolchain-version] -compiler-version = "1.33.0" -edition = "2024.beta" -flavor = "sui" - -[env] - -[env.testnet] -chain-id = "4c78adac" -original-published-id = "0x2c6aa312fbba13c0184b10a53273b58fda1e9f6119ce8a55fd2d7ea452c56bd8" -latest-published-id = "0x2c6aa312fbba13c0184b10a53273b58fda1e9f6119ce8a55fd2d7ea452c56bd8" -published-version = "1" - -[env.mainnet] -chain-id = "35834a8a" -original-published-id = "0x6ad6692327074e360e915401812aa3192e88f6effb02b3d67a970e57dd11f1b0" -latest-published-id = "0x6ad6692327074e360e915401812aa3192e88f6effb02b3d67a970e57dd11f1b0" -published-version = "1""#, - ) -} - fn update_mvr_packages(move_toml_path: &Path, package_name: &str, network: &str) -> Result<()> { let toml_content = fs::read_to_string(&move_toml_path) .with_context(|| format!("Failed to read file: {:?}", move_toml_path))?; diff --git a/mvr-cli/src/tests/unit_tests.rs b/mvr-cli/src/tests/unit_tests.rs index 54b8bfc..f386a0b 100644 --- a/mvr-cli/src/tests/unit_tests.rs +++ b/mvr-cli/src/tests/unit_tests.rs @@ -1,21 +1,360 @@ use anyhow::Result; +use expect_test::expect; use mvr::{ - resolve_move_dependencies, subcommand_add_dependency, subcommand_list, - subcommand_register_name, subcommand_resolve_name, PackageInfoNetwork, + build_lock_files, check_address_consistency, parse_package_version, resolve_move_dependencies, + subcommand_add_dependency, subcommand_list, subcommand_register_name, subcommand_resolve_name, + GitInfo, PackageInfo, PackageInfoNetwork, }; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use sui_sdk::types::base_types::ObjectID; +use tempfile::tempdir; + +fn create_resolved_packages() -> HashMap { + let mut resolved_packages = HashMap::new(); + resolved_packages.insert( + "@mvr-test/first-app/1".to_string(), + PackageInfo { + upgrade_cap_id: ObjectID::random(), + package_address: ObjectID::from_hex_literal("0x1234567890abcdef").unwrap(), + git_versioning: { + let mut map = HashMap::new(); + map.insert( + 1, + GitInfo { + repository: "https://github.com/example/demo.git".to_string(), + tag: "v1.0.0".to_string(), + path: "packages/demo".to_string(), + }, + ); + map + }, + }, + ); + resolved_packages +} #[tokio::test] -async fn test_resolve_move_dependencies() -> Result<()> { +async fn test_build_lock_files() -> Result<()> { + let temp_dir = tempdir()?; + let move_toml_content = r#" +[package] +name = "demo" +edition = "2024.beta" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } + +[addresses] +demo = "0x0" +"#; + + let move_lock_content = r#"# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "0" +deps_digest = "0" +dependencies = [ + { id = "Sui", name = "Sui" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] +"#; + + let move_toml_path = temp_dir.path().join("Move.toml"); + let move_lock_path = temp_dir.path().join("Move.lock"); + fs::write(&move_toml_path, move_toml_content)?; + fs::write(&move_lock_path, move_lock_content)?; + + let resolved_packages = create_resolved_packages(); + let mut fetched_files = HashMap::new(); + fetched_files.insert( + "@mvr-test/first-app/1".to_string(), + (move_toml_path, move_lock_path), + ); + let result = build_lock_files(&resolved_packages, &fetched_files).await?; + + // Note: roots "demo" in `move.dependencies` and creates a [move.package.source] entry for the demo package. + expect![[r##" + # @generated by Move, please check-in and do not edit manually. + + [move] + version = 3 + manifest_digest = "0" + deps_digest = "0" + dependencies = [{ name = "demo", id = "demo" }] + + [[move.package]] + id = "MoveStdlib" + source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } + + [[move.package]] + id = "Sui" + source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } + + dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + ] + + [[move.package]] + id = "demo" + dependencies = [ + { id = "Sui", name = "Sui" }, + ] + + [move.package.source] + git = "https://github.com/example/demo.git" + rev = "v1.0.0" + subdir = "packages/demo" + "##]] + .assert_eq(&result[0]); Ok(()) } #[tokio::test] -async fn test_subcommand_list() -> Result<()> { +async fn test_parse_package_version() -> Result<()> { + let result = parse_package_version("@foo/bar/1")?; + expect![[r#" + ( + "@foo/bar", + Some( + 1, + ), + ) + "#]] + .assert_debug_eq(&result); + + let result = parse_package_version("foo/bar")?; + expect![[r#" + ( + "foo/bar", + None, + ) + "#]] + .assert_debug_eq(&result); + + let result = parse_package_version("@foo/bar")?; + expect![[r#" + ( + "@foo/bar", + None, + ) + "#]] + .assert_debug_eq(&result); + + let result = parse_package_version("@foo/bar/baz"); + expect![[r#" + Err( + "Cannot parse version \"baz\". Version must be 1 or greater.", + ) + "#]] + .assert_debug_eq(&result); + + let result = parse_package_version("@foo/bar/0"); + expect![[r#" + Err( + "Invalid version number 0. Version must be 1 or greater.", + ) + "#]] + .assert_debug_eq(&result); Ok(()) } -#[test] -fn test_package_info_network() { - assert_eq!(PackageInfoNetwork::Mainnet.to_string(), "mainnet"); - assert_eq!(PackageInfoNetwork::Testnet.to_string(), "testnet"); +#[tokio::test] +async fn test_check_address_consistency() -> Result<()> { + let temp_dir = tempdir()?; + /////////////// + // Move.toml // + /////////////// + let move_toml_content = r#" +[package] +name = "demo" +version = "0.0.1" + +[addresses] +demo = "0x0" +"#; + + /////////////// + // Move.lock // + /////////////// + let move_lock_content = r#" +[move] +version = 3 +dependencies = [ + { name = "Sui", addr = "0x2" }, +] + +[env] + +[env.testnet] +chain-id = "4c78adac" +original-published-id = "0x1234567890abcdef" +latest-published-id = "0x1234567890abcdef" +published-version = "1" +"#; + + let move_toml_path = temp_dir.path().join("Move.toml"); + let move_lock_path = temp_dir.path().join("Move.lock"); + fs::write(&move_toml_path, move_toml_content)?; + fs::write(&move_lock_path, move_lock_content)?; + + let resolved_packages = create_resolved_packages(); + let mut fetched_files = HashMap::new(); + fetched_files.insert( + "@mvr-test/first-app/1".to_string(), + (move_toml_path.clone(), move_lock_path.clone()), + ); + //////////////////////////////////////////////////////////////////////////////////////////// + // Expect success: Move.lock matches expected resolved address. // + //////////////////////////////////////////////////////////////////////////////////////////// + let network = PackageInfoNetwork::Testnet; + let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; + expect![[r#" + Ok( + (), + ) + "#]] + .assert_debug_eq(&result); + + //////////////////////////////////////////////////////////////////////////////////////////// + // Expect failure: Move.lock does not match resolved address. // + //////////////////////////////////////////////////////////////////////////////////////////// + + /////////////// + // Move.lock // + /////////////// + let move_lock_content = r#" +[move] +version = 3 +dependencies = [ + { name = "Sui", addr = "0x2" }, +] + +[env] + +[env.testnet] +chain-id = "4c78adac" +original-published-id = "0xbad" +latest-published-id = "0xbad" +published-version = "1" +"#; + + let move_lock_path = temp_dir.path().join("Move.lock"); + fs::write(&move_lock_path, move_lock_content)?; + fetched_files.insert( + "@mvr-test/first-app/1".to_string(), + (move_toml_path.clone(), move_lock_path), + ); + let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; + expect![[r#" + Err( + "Mismatch: The original package address for @mvr-test/first-app on testnet is 0x0000000000000000000000000000000000000000000000001234567890abcdef, but the Move.lock in @mvr-test/first-app's repository was found to be 0x0000000000000000000000000000000000000000000000000000000000000bad.\nCheck the configuration of the package's repository https://github.com/example/demo.git in branch v1.0.0 in subdirectory packages/demo", + ) + "#]] + .assert_debug_eq(&result); + + //////////////////////////////////////////////////////////////////////////////////////////// + // Expect failure: no published address (empty lock and no address in Move.toml) // + //////////////////////////////////////////////////////////////////////////////////////////// + + /////////////// + // Move.lock // + /////////////// + let move_lock_content = r#" +[move] +version = 3 +dependencies = [ + { name = "Sui", addr = "0x2" }, +] +"#; + + let move_lock_path = temp_dir.path().join("Move.lock"); + fs::write(&move_lock_path, move_lock_content)?; + fetched_files.insert( + "@mvr-test/first-app/1".to_string(), + (move_toml_path.clone(), move_lock_path.clone()), + ); + let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; + expect![[r#" + Err( + "Mismatch: The original package address for @mvr-test/first-app on testnet is 0x0000000000000000000000000000000000000000000000001234567890abcdef, but the address in the [addresses] section of the Move.toml in @mvr-test/first-app's repository was found to be 0x0000000000000000000000000000000000000000000000000000000000000000.\nCheck the configuration of the package's repository https://github.com/example/demo.git in branch v1.0.0 in subdirectory packages/demo", + ) + "#]] + .assert_debug_eq(&result); + + //////////////////////////////////////////////////////////////////////////////////////////// + // Expect success: address resolved from published-at (0x0 in [addresses] // + //////////////////////////////////////////////////////////////////////////////////////////// + + /////////////// + // Move.toml // + /////////////// + let move_toml_content = r#" +[package] +name = "demo" +published-at = "0x1234567890abcdef" +version = "0.0.1" + +[addresses] +demo = "0x0" +"#; + + let move_toml_path = temp_dir.path().join("Move.toml"); + fs::write(&move_toml_path, move_toml_content)?; + fetched_files.insert( + "@mvr-test/first-app/1".to_string(), + (move_toml_path, move_lock_path.clone()), + ); + let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; + expect![[r#" + Ok( + (), + ) + "#]] + .assert_debug_eq(&result); + + //////////////////////////////////////////////////////////////////////////////////////////// + // Expect success: address resolved from [addresses] (published-at is upgraded pkg addr) // + //////////////////////////////////////////////////////////////////////////////////////////// + + /////////////// + // Move.toml // + /////////////// + let move_toml_content = r#" +[package] +name = "demo" +published-at = "0xabcdef" +version = "0.0.1" + +[addresses] +demo = "0x1234567890abcdef" +"#; + + let move_toml_path = temp_dir.path().join("Move.toml"); + fs::write(&move_toml_path, move_toml_content)?; + fetched_files.insert( + "@mvr-test/first-app/1".to_string(), + (move_toml_path, move_lock_path), + ); + let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; + expect![[r#" + Ok( + (), + ) + "#]] + .assert_debug_eq(&result); + Ok(()) }