1use anyhow::{Context, Result};
2use std::fs;
3use zint::utils::{find_up, FoundryConfig};
4use zint::Contract;
5
6pub fn create_ztests_crate() -> Result<()> {
7 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 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 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 let zink_version = std::env::var("ZINK_VERSION").unwrap_or_else(|_| "0.1.12".to_string());
28
29 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 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 let outputs = Contract::find_foundry_outputs()?;
62 if outputs.is_empty() {
63 println!("No Foundry outputs found");
64 }
65
66 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 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 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 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 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 let ztests_path = find_up("ztests/Cargo.toml")?
185 .parent()
186 .unwrap()
187 .to_path_buf();
188
189 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}