use anyhow::{Context, Result}; use std::fs; use zint::utils::{find_up, FoundryConfig}; use zint::Contract; pub fn create_ztests_crate() -> Result<()> { // Find foundry.toml in the current directory or parent directories let foundry_toml_path = find_up("foundry.toml")?; let foundry_toml_content = fs::read_to_string(&foundry_toml_path).context("Failed to read foundry.toml")?; let foundry_config: FoundryConfig = toml::from_str(&foundry_toml_content).context("Failed to parse foundry.toml")?; // Determine the output directory (default to "out" if not specified) let out_dir = foundry_config .profile .default .out .unwrap_or_else(|| "out".to_string()); let _out_path = foundry_toml_path.parent().unwrap().join(&out_dir); // Create ztests directory let ztests_path = foundry_toml_path.parent().unwrap().join("ztests"); fs::create_dir_all(&ztests_path).context("Failed to create ztests directory")?; // Fetch the zink version from the environment variable set by zint-cli's build.rs let zink_version = std::env::var("ZINK_VERSION").unwrap_or_else(|_| "0.1.12".to_string()); // Write ztests/Cargo.toml with workspace dependencies let cargo_toml_content = format!( r#" [package] name = "ztests" version = "0.1.0" edition = "2021" [dependencies] anyhow = "1.0" zink = "{zink_version}" zint = "^0.1" [lib] doctest = false [features] abi-import = ["zink/abi-import"] [workspace] "#, zink_version = zink_version ); fs::write(ztests_path.join("Cargo.toml"), cargo_toml_content) .context("Failed to write ztests/Cargo.toml")?; // Create ztests/src directory let ztests_src_path = ztests_path.join("src"); fs::create_dir_all(&ztests_src_path).context("Failed to create ztests/src directory")?; // Use find_foundry_outputs() to get the list of contract artifacts let outputs = Contract::find_foundry_outputs()?; if outputs.is_empty() { println!("No Foundry outputs found"); } // Find all .json files in the out directory (compiled contract ABIs) let mut test_file_content = String::from( r#"#[cfg(test)] mod tests { #[cfg(feature = "abi-import")] use zink::import; #[allow(unused_imports)] use zink::primitives::address::Address; #[allow(unused_imports)] use zink::primitives::u256::U256; "#, ); for (contract_name, abi_path, _bytecode) in outputs { let file_name = abi_path.file_name().unwrap().to_str().unwrap(); let contract_struct_name = contract_name; // Parse the ABI to generate specific tests let abi_content = fs::read_to_string(&abi_path)?; let abi: serde_json::Value = match serde_json::from_str(&abi_content) { Ok(abi) => abi, Err(e) => { println!("Failed to parse ABI for {}: {}", file_name, e); continue; } }; let abi_array = match abi["abi"].as_array() { Some(array) => array, None => { println!("No 'abi' field found in {}: {:?}", file_name, abi); continue; } }; let mut test_body = String::new(); for item in abi_array { let fn_name = item["name"].as_str().unwrap_or(""); let fn_type = item["type"].as_str().unwrap_or(""); let state_mutability = item["stateMutability"].as_str().unwrap_or(""); let inputs = item["inputs"].as_array().unwrap_or(&vec![]).to_vec(); let outputs = item["outputs"].as_array().unwrap_or(&vec![]).to_vec(); if fn_type != "function" { continue; } // Generate test logic based on the function if fn_name == "set" && inputs.len() == 1 && inputs[0]["type"] == "uint256" { test_body.push_str( r#" contract.set(U256::from(42))?; "#, ); } else if fn_name == "get" && outputs.len() == 1 && outputs[0]["type"] == "uint256" && state_mutability == "view" { test_body.push_str( r#" let retrieved = contract.get()?; println!("Retrieved value via get: {:?}", retrieved); assert_eq!(retrieved, U256::from(42)); "#, ); } } // Only add the test if we generated some test body if !test_body.is_empty() { test_file_content.push_str(&format!( r#" #[test] fn test_{contract_struct_name}() -> anyhow::Result<()> {{ #[cfg(feature = "abi-import")] {{ import!("{out_dir}/Storage.sol/{file_name}"); let contract_address = Address::from(zint::primitives::CONTRACT); println!("Contract address: {{contract_address:?}}"); let contract = {contract_struct_name}::new(contract_address); {test_body} // Check storage directly let mut evm = contract.evm.clone(); let storage_key = zink::storage::Mapping::<u8, U256>::storage_key(0); let stored_value = U256::from_be_bytes( evm.storage(*contract_address.as_bytes(), storage_key).unwrap_or([0u8; 32]) ); println!("Stored value in EVM: {{stored_value:?}}"); assert_eq!(stored_value, U256::from(42)); Ok(()) }} #[cfg(not(feature = "abi-import"))] {{ println!("Test skipped: abi-import feature not enabled"); Ok(()) }} }} "#, )); } else { println!("No testable functions found in ABI for {}", file_name); } } test_file_content.push_str("}\n"); // Write ztests/src/lib.rs fs::write(ztests_src_path.join("lib.rs"), test_file_content) .context("Failed to write ztests/src/lib.rs")?; println!("Created ztests crate at {}", ztests_path.display()); Ok(()) } pub fn run_ztests() -> Result<()> { // Find ztests directory let ztests_path = find_up("ztests/Cargo.toml")? .parent() .unwrap() .to_path_buf(); // Deploy contracts before running tests let outputs = Contract::find_foundry_outputs()?; if outputs.is_empty() { println!("No Foundry outputs found"); return Err(anyhow::anyhow!("No contracts to deploy")); } for (contract_name, _abi_path, bytecode) in outputs { println!( "Deploying contract {} with bytecode size {}", contract_name, bytecode.len() ); let mut contract = Contract { wasm: bytecode, ..Default::default() }; let evm = contract.deploy()?; evm.commit(true); println!( "Deployed contract {} at address {:?}", contract_name, contract.address ); } let status = std::process::Command::new("cargo") .args(["nextest", "run", "--manifest-path", "ztests/Cargo.toml"]) .current_dir(ztests_path.parent().unwrap()) .status() .context("Failed to run cargo nextest")?; if !status.success() { anyhow::bail!("Tests failed"); } Ok(()) }