use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
};
use anyhow::Ok;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde_json::from_str;
use crate::{
arguments,
common::search_file_recursively,
dfx_json::add_canisters,
foundry_toml::{add_contract, add_contracts},
};
use super::arguments::IcpTestArgs;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EvmSetup {
#[serde(default)]
pub foundry_toml_path: String,
#[serde(default)]
pub skip_foundry_toml: bool,
#[serde(default)]
pub foundry_src: String,
#[serde(default)]
pub foundry_out: String,
#[serde(default)]
pub contracts: BTreeMap<String, ContractSetup>,
}
impl EvmSetup {
pub fn get_foundry_toml(&self) -> PathBuf {
PathBuf::new()
.join(self.foundry_toml_path.clone())
.join(crate::common::FOUNDRY_TOML)
}
pub fn get_foundry_src(&self) -> PathBuf {
PathBuf::new()
.join(self.foundry_toml_path.clone())
.join(self.foundry_src.clone())
}
pub fn get_foundry_out(&self) -> PathBuf {
PathBuf::new()
.join(self.foundry_toml_path.clone())
.join(self.foundry_out.clone())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IcpSetup {
#[serde(skip_serializing_if = "Option::is_none")]
pub dfx_json: Option<String>,
pub skip_dfx_json: bool,
pub canisters: BTreeMap<String, CanisterSetup>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IcpTestSetup {
pub test_folder: String,
#[serde(skip)]
pub forced: bool,
#[serde(skip)]
pub regenerate_cargo: bool,
#[serde(skip)]
pub is_complete: bool,
#[serde(skip)]
pub ui: bool,
#[serde(skip)]
pub test_rs_regenerated: bool,
pub icp_setup: IcpSetup,
#[serde(skip_serializing_if = "Option::is_none")]
pub evm_setup: Option<EvmSetup>,
}
impl Default for IcpSetup {
fn default() -> Self {
Self {
dfx_json: Some("dfx.json".to_string()),
skip_dfx_json: false,
canisters: BTreeMap::new(),
}
}
}
impl Default for EvmSetup {
fn default() -> Self {
Self {
foundry_toml_path: "".to_string(),
foundry_src: "src".to_string(),
foundry_out: "out".to_string(),
skip_foundry_toml: false,
contracts: BTreeMap::new(),
}
}
}
impl Default for IcpTestSetup {
fn default() -> Self {
Self {
test_folder: "tests".to_string(),
forced: false,
regenerate_cargo: false,
icp_setup: IcpSetup::default(),
evm_setup: None,
is_complete: false,
test_rs_regenerated: false,
ui: false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CanisterSetup {
pub name: String,
pub var_name: String,
pub service_name: String,
#[serde(skip)]
pub init_args_rust: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub candid_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub init_arg_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub init_arg: Option<String>,
#[serde(default)]
pub generate_bindings: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub wasm: Option<String>,
pub specified_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContractSetup {
pub name: String,
pub var_name: String,
pub sol_json: String,
}
pub fn init_test_config(args: &IcpTestArgs) -> anyhow::Result<IcpTestSetup> {
let ic_test_json_path = Path::new(&args.ic_test_json);
let mut setup = if !ic_test_json_path.exists() {
IcpTestSetup::default()
} else {
let json_string = fs::read_to_string(&args.ic_test_json)?;
from_str::<IcpTestSetup>(&json_string)?
};
if let Some(_evm) = &mut setup.evm_setup {
} else {
debug!("Searching for foundry.toml...");
let foundry_toml = search_file_recursively(Path::new("."), 3, crate::common::FOUNDRY_TOML);
if let Some(path) = &foundry_toml {
debug!("foundry.toml found: {path:?}");
} else {
debug!("foundry.toml not found!");
}
if let Some(f) = foundry_toml {
let path = f.parent().expect("could not find foundry.toml parent");
let evm = EvmSetup {
foundry_toml_path: path.to_string_lossy().to_string(),
..EvmSetup::default()
};
setup.evm_setup = Some(evm);
}
}
setup.ui = args.ui == Some(true);
if let Some(dfx_json) = &args.dfx_json {
if dfx_json.is_empty() {
setup.icp_setup.dfx_json = None;
} else {
setup.icp_setup.dfx_json = Some(dfx_json.to_owned());
}
}
if let Some(skip) = args.skip_dfx_json {
setup.icp_setup.skip_dfx_json = skip;
}
if let Some(evm_setup) = &mut setup.evm_setup {
if let Some(skip) = args.skip_foundry_toml {
evm_setup.skip_foundry_toml = skip;
}
}
debug!("automatically adding canisters...");
add_canisters(&mut setup)?;
debug!("automatically adding contracts...");
add_contracts(&mut setup)?;
debug!("processing command {:?}", &args.command);
match &args.command {
arguments::Command::New { test_folder, force } => {
setup.test_folder = test_folder.clone();
setup.forced = *force;
}
arguments::Command::Update {
force,
name,
wasm,
init_arg_file,
init_arg,
sol_json,
} => {
setup.forced = *force;
if let Some(name) = name {
let canister = setup.icp_setup.canisters.get_mut(name);
if let Some(canister) = canister {
if let Some(init_arg_file) = init_arg_file {
if init_arg_file.is_empty() {
canister.init_arg_file = None;
} else {
canister.init_arg_file = Some(init_arg_file.clone());
}
}
if let Some(init_arg) = init_arg {
if init_arg.is_empty() {
canister.init_arg_file = None;
} else {
canister.init_arg_file = Some(init_arg.clone());
}
}
if let Some(wasm) = wasm {
canister.wasm = Some(wasm.clone());
}
}
if let Some(evm_setup) = &mut setup.evm_setup {
let contract = evm_setup.contracts.get_mut(name);
if let Some(contract) = contract {
if let Some(sol_json) = sol_json {
contract.sol_json = sol_json.clone();
}
}
}
}
}
arguments::Command::Add { command } => {
match command {
arguments::AddCommand::Canister {
name,
wasm: _,
init_arg_file: _,
init_arg: _,
} => {
info!("Adding canister {name}");
todo!("Adding a canister is currently not supported. To add one manually, modify the ic-test.json file directly.");
}
arguments::AddCommand::Contract { name, sol_json } => {
add_contract(name, sol_json, &mut setup)?;
}
}
}
}
Ok(setup)
}
pub fn store_test_config(args: &IcpTestArgs, setup: &IcpTestSetup) -> anyhow::Result<()> {
let to_store = serde_json::to_string_pretty(&setup)?;
fs::write(&args.ic_test_json, to_store)?;
Ok(())
}