zint_cli/
cmd.rs

1use anyhow::{Context, Result};
2use std::fs;
3use zint::utils::{find_up, FoundryConfig};
4use zint::Contract;
5
6pub fn create_ztests_crate() -> Result<()> {
7    // Find foundry.toml in the current directory or parent directories
8    let foundry_toml_path = find_up("foundry.toml")?;
9    let foundry_toml_content =
10        fs::read_to_string(&foundry_toml_path).context("Failed to read foundry.toml")?;
11    let foundry_config: FoundryConfig =
12        toml::from_str(&foundry_toml_content).context("Failed to parse foundry.toml")?;
13
14    // Determine the output directory (default to "out" if not specified)
15    let out_dir = foundry_config
16        .profile
17        .default
18        .out
19        .unwrap_or_else(|| "out".to_string());
20    let _out_path = foundry_toml_path.parent().unwrap().join(&out_dir);
21
22    // Create ztests directory
23    let ztests_path = foundry_toml_path.parent().unwrap().join("ztests");
24    fs::create_dir_all(&ztests_path).context("Failed to create ztests directory")?;
25
26    // Fetch the zink version from the environment variable set by zint-cli's build.rs
27    let zink_version = std::env::var("ZINK_VERSION").unwrap_or_else(|_| "0.1.12".to_string());
28
29    // Write ztests/Cargo.toml with workspace dependencies
30    let cargo_toml_content = format!(
31        r#"
32[package]
33name = "ztests"
34version = "0.1.0"
35edition = "2021"
36
37[dependencies]
38anyhow = "1.0"
39zink = "{zink_version}"
40zint = "^0.1"
41
42
43[lib]
44doctest = false
45
46[features]
47abi-import = ["zink/abi-import"]
48
49[workspace]
50"#,
51        zink_version = zink_version
52    );
53    fs::write(ztests_path.join("Cargo.toml"), cargo_toml_content)
54        .context("Failed to write ztests/Cargo.toml")?;
55
56    // Create ztests/src directory
57    let ztests_src_path = ztests_path.join("src");
58    fs::create_dir_all(&ztests_src_path).context("Failed to create ztests/src directory")?;
59
60    // Use find_foundry_outputs() to get the list of contract artifacts
61    let outputs = Contract::find_foundry_outputs()?;
62    if outputs.is_empty() {
63        println!("No Foundry outputs found");
64    }
65
66    // Find all .json files in the out directory (compiled contract ABIs)
67    let mut test_file_content = String::from(
68        r#"#[cfg(test)]
69mod tests {
70    #[cfg(feature = "abi-import")]
71    use zink::import;
72    
73    #[allow(unused_imports)]
74    use zink::primitives::address::Address;
75    #[allow(unused_imports)]
76    use zink::primitives::u256::U256;
77
78"#,
79    );
80
81    for (contract_name, abi_path, _bytecode) in outputs {
82        let file_name = abi_path.file_name().unwrap().to_str().unwrap();
83        let contract_struct_name = contract_name;
84
85        // Parse the ABI to generate specific tests
86        let abi_content = fs::read_to_string(&abi_path)?;
87        let abi: serde_json::Value = match serde_json::from_str(&abi_content) {
88            Ok(abi) => abi,
89            Err(e) => {
90                println!("Failed to parse ABI for {}: {}", file_name, e);
91                continue;
92            }
93        };
94        let abi_array = match abi["abi"].as_array() {
95            Some(array) => array,
96            None => {
97                println!("No 'abi' field found in {}: {:?}", file_name, abi);
98                continue;
99            }
100        };
101
102        let mut test_body = String::new();
103        for item in abi_array {
104            let fn_name = item["name"].as_str().unwrap_or("");
105            let fn_type = item["type"].as_str().unwrap_or("");
106            let state_mutability = item["stateMutability"].as_str().unwrap_or("");
107            let inputs = item["inputs"].as_array().unwrap_or(&vec![]).to_vec();
108            let outputs = item["outputs"].as_array().unwrap_or(&vec![]).to_vec();
109
110            if fn_type != "function" {
111                continue;
112            }
113
114            // Generate test logic based on the function
115            if fn_name == "set" && inputs.len() == 1 && inputs[0]["type"] == "uint256" {
116                test_body.push_str(
117                    r#"
118            contract.set(U256::from(42))?;
119"#,
120                );
121            } else if fn_name == "get"
122                && outputs.len() == 1
123                && outputs[0]["type"] == "uint256"
124                && state_mutability == "view"
125            {
126                test_body.push_str(
127                    r#"
128            let retrieved = contract.get()?;
129            println!("Retrieved value via get: {:?}", retrieved);
130            assert_eq!(retrieved, U256::from(42));
131"#,
132                );
133            }
134        }
135
136        // Only add the test if we generated some test body
137        if !test_body.is_empty() {
138            test_file_content.push_str(&format!(
139                r#"
140    #[test]
141    fn test_{contract_struct_name}() -> anyhow::Result<()> {{
142        #[cfg(feature = "abi-import")]
143        {{
144            import!("{out_dir}/Storage.sol/{file_name}");
145            let contract_address = Address::from(zint::primitives::CONTRACT);
146            println!("Contract address: {{contract_address:?}}");
147            let contract = {contract_struct_name}::new(contract_address);
148            {test_body}
149            // Check storage directly
150            let mut evm = contract.evm.clone();
151            let storage_key = zink::storage::Mapping::<u8, U256>::storage_key(0);
152            let stored_value = U256::from_be_bytes(
153                evm.storage(*contract_address.as_bytes(), storage_key).unwrap_or([0u8; 32])
154            );
155            println!("Stored value in EVM: {{stored_value:?}}");
156            assert_eq!(stored_value, U256::from(42));
157            Ok(())
158        }}
159        #[cfg(not(feature = "abi-import"))]
160        {{
161            println!("Test skipped: abi-import feature not enabled");
162            Ok(())
163        }}
164    }}
165"#,
166            ));
167        } else {
168            println!("No testable functions found in ABI for {}", file_name);
169        }
170    }
171
172    test_file_content.push_str("}\n");
173
174    // Write ztests/src/lib.rs
175    fs::write(ztests_src_path.join("lib.rs"), test_file_content)
176        .context("Failed to write ztests/src/lib.rs")?;
177
178    println!("Created ztests crate at {}", ztests_path.display());
179    Ok(())
180}
181
182pub fn run_ztests() -> Result<()> {
183    // Find ztests directory
184    let ztests_path = find_up("ztests/Cargo.toml")?
185        .parent()
186        .unwrap()
187        .to_path_buf();
188
189    // Deploy contracts before running tests
190    let outputs = Contract::find_foundry_outputs()?;
191    if outputs.is_empty() {
192        println!("No Foundry outputs found");
193        return Err(anyhow::anyhow!("No contracts to deploy"));
194    }
195    for (contract_name, _abi_path, bytecode) in outputs {
196        println!(
197            "Deploying contract {} with bytecode size {}",
198            contract_name,
199            bytecode.len()
200        );
201        let mut contract = Contract {
202            wasm: bytecode,
203            ..Default::default()
204        };
205        let evm = contract.deploy()?;
206        evm.commit(true);
207        println!(
208            "Deployed contract {} at address {:?}",
209            contract_name, contract.address
210        );
211    }
212
213    let status = std::process::Command::new("cargo")
214        .args(["nextest", "run", "--manifest-path", "ztests/Cargo.toml"])
215        .current_dir(ztests_path.parent().unwrap())
216        .status()
217        .context("Failed to run cargo nextest")?;
218
219    if !status.success() {
220        anyhow::bail!("Tests failed");
221    }
222
223    Ok(())
224}