feat: handle errors in protocol-testing

This commit is contained in:
adrian
2025-09-02 16:25:49 +02:00
committed by Tamara
parent f3500dff44
commit 8205c2a8d5
6 changed files with 126 additions and 79 deletions

View File

@@ -596,7 +596,7 @@ dependencies = [
"alloy-serde", "alloy-serde",
"alloy-sol-types", "alloy-sol-types",
"arbitrary", "arbitrary",
"itertools 0.13.0", "itertools 0.14.0",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@@ -909,7 +909,7 @@ checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"memchr", "memchr",
"unicode-width", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@@ -1752,7 +1752,7 @@ dependencies = [
"strsim", "strsim",
"terminal_size", "terminal_size",
"unicase", "unicase",
"unicode-width", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@@ -1870,7 +1870,7 @@ checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@@ -1882,7 +1882,7 @@ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
"once_cell", "once_cell",
"unicode-width", "unicode-width 0.2.0",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -2382,7 +2382,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -2919,7 +2919,7 @@ dependencies = [
"foundry-compilers-core", "foundry-compilers-core",
"futures-util", "futures-util",
"home", "home",
"itertools 0.13.0", "itertools 0.14.0",
"path-slash", "path-slash",
"rayon", "rayon",
"semver 1.0.26", "semver 1.0.26",
@@ -4020,7 +4020,7 @@ dependencies = [
"console", "console",
"number_prefix", "number_prefix",
"portable-atomic", "portable-atomic",
"unicode-width", "unicode-width 0.2.0",
"web-time", "web-time",
] ]
@@ -4493,6 +4493,28 @@ dependencies = [
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
[[package]]
name = "miette"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
dependencies = [
"cfg-if",
"miette-derive",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -5339,7 +5361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.13.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.99", "syn 2.0.99",
@@ -5376,6 +5398,7 @@ dependencies = [
"dotenv", "dotenv",
"figment", "figment",
"hex", "hex",
"miette",
"postgres", "postgres",
"serde", "serde",
"serde_json", "serde_json",
@@ -6723,7 +6746,7 @@ dependencies = [
"derive_more 2.0.1", "derive_more 2.0.1",
"dunce", "dunce",
"inturn", "inturn",
"itertools 0.13.0", "itertools 0.14.0",
"itoa", "itoa",
"match_cfg", "match_cfg",
"normalize-path", "normalize-path",
@@ -6734,7 +6757,7 @@ dependencies = [
"solar-macros", "solar-macros",
"thiserror 2.0.12", "thiserror 2.0.12",
"tracing", "tracing",
"unicode-width", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@@ -6757,7 +6780,7 @@ dependencies = [
"alloy-primitives", "alloy-primitives",
"bitflags 2.9.4", "bitflags 2.9.4",
"bumpalo", "bumpalo",
"itertools 0.13.0", "itertools 0.14.0",
"memchr", "memchr",
"num-bigint", "num-bigint",
"num-rational", "num-rational",
@@ -7122,7 +7145,7 @@ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix 1.0.1", "rustix 1.0.1",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -7891,6 +7914,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.0" version = "0.2.0"

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
miette = "7.6.0"
# Logging & Tracing # Logging & Tracing
tracing = "0.1.37" tracing = "0.1.37"
# Tycho dependencies # Tycho dependencies

View File

@@ -30,7 +30,7 @@ struct Args {
vm_traces: bool, vm_traces: bool,
} }
fn main() { fn main() -> miette::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.with_target(false) .with_target(false)
@@ -40,5 +40,5 @@ fn main() {
let test_runner = TestRunner::new(args.package, args.tycho_logs, args.db_url, args.vm_traces); let test_runner = TestRunner::new(args.package, args.tycho_logs, args.db_url, args.vm_traces);
test_runner.run_tests(); test_runner.run_tests()
} }

View File

@@ -1,17 +1,19 @@
use std::{collections::HashMap, env, path::PathBuf, str::FromStr}; use std::{collections::HashMap, env, path::PathBuf, str::FromStr};
use alloy::{primitives::U256};
use alloy::primitives::U256;
use figment::{ use figment::{
providers::{Format, Yaml}, providers::{Format, Yaml},
Figment, Figment,
}; };
use miette::{miette, IntoDiagnostic, WrapErr};
use postgres::{Client, Error, NoTls}; use postgres::{Client, Error, NoTls};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tracing::{debug, info}; use tracing::{debug, info};
use tycho_client::feed::BlockHeader; use tycho_client::feed::BlockHeader;
use tycho_common::{ use tycho_common::{
dto::{Chain, ProtocolComponent, ResponseAccount, ResponseProtocolState}, dto::{Chain, ProtocolComponent, ResponseAccount, ResponseProtocolState},
models::token::Token,
Bytes, Bytes,
models::token::Token
}; };
use tycho_simulation::{ use tycho_simulation::{
evm::{ evm::{
@@ -47,7 +49,7 @@ impl TestRunner {
Self { package, tycho_logs, db_url, vm_traces, substreams_path } Self { package, tycho_logs, db_url, vm_traces, substreams_path }
} }
pub fn run_tests(&self) { pub fn run_tests(&self) -> miette::Result<()> {
info!("Running tests..."); info!("Running tests...");
let config_yaml_path = self let config_yaml_path = self
.substreams_path .substreams_path
@@ -56,20 +58,21 @@ impl TestRunner {
info!("Config YAML: {}", config_yaml_path.display()); info!("Config YAML: {}", config_yaml_path.display());
let figment = Figment::new().merge(Yaml::file(&config_yaml_path)); let figment = Figment::new().merge(Yaml::file(&config_yaml_path));
match figment.extract::<IntegrationTestsConfig>() { let config = figment
Ok(config) => { .extract::<IntegrationTestsConfig>()
info!("Loaded test configuration:"); .into_diagnostic()
info!("Protocol types: {:?}", config.protocol_type_names); .wrap_err("Failed to load test configuration:")?;
info!("Found {} tests to run", config.tests.len()); info!("Loaded test configuration:");
info!("Protocol types: {:?}", config.protocol_type_names);
info!("Found {} tests to run", config.tests.len());
for test in &config.tests { for test in &config.tests {
self.run_test(test, &config, config.skip_balance_check); if let Err(e) = self.run_test(test, &config, config.skip_balance_check) {
} eprintln!("Test '{}' failed: {e}", test.name);
}
Err(e) => {
eprintln!("Failed to load test configuration: {}", e);
} }
} }
Ok(())
} }
fn run_test( fn run_test(
@@ -77,10 +80,11 @@ impl TestRunner {
test: &IntegrationTest, test: &IntegrationTest,
config: &IntegrationTestsConfig, config: &IntegrationTestsConfig,
skip_balance_check: bool, skip_balance_check: bool,
) { ) -> miette::Result<()> {
info!("Running test: {}", test.name); info!("Running test: {}", test.name);
self.empty_database() self.empty_database()
.expect("Failed to empty the database"); .into_diagnostic()
.wrap_err("Failed to empty the database")?;
let substreams_yaml_path = self let substreams_yaml_path = self
.substreams_path .substreams_path
@@ -98,7 +102,7 @@ impl TestRunner {
); );
let spkg_path = let spkg_path =
build_spkg(&substreams_yaml_path, test.start_block).expect("Failed to build spkg"); build_spkg(&substreams_yaml_path, test.start_block).wrap_err("Failed to build spkg")?;
let tycho_runner = let tycho_runner =
TychoRunner::new(self.db_url.clone(), initialized_accounts, self.tycho_logs); TychoRunner::new(self.db_url.clone(), initialized_accounts, self.tycho_logs);
@@ -110,15 +114,17 @@ impl TestRunner {
test.stop_block, test.stop_block,
&config.protocol_type_names, &config.protocol_type_names,
) )
.expect("Failed to run Tycho"); .wrap_err("Failed to run Tycho")?;
tycho_runner.run_with_rpc_server( let _ = tycho_runner.run_with_rpc_server(
validate_state, validate_state,
&test.expected_components, &test.expected_components,
test.start_block, test.start_block,
test.stop_block, test.stop_block,
skip_balance_check, skip_balance_check,
); )?;
Ok(())
} }
fn empty_database(&self) -> Result<(), Error> { fn empty_database(&self) -> Result<(), Error> {
@@ -144,12 +150,13 @@ fn validate_state(
start_block: u64, start_block: u64,
stop_block: u64, stop_block: u64,
skip_balance_check: bool, skip_balance_check: bool,
) { ) -> miette::Result<()> {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
// Create Tycho client for the RPC server // Create Tycho client for the RPC server
let tycho_client = let tycho_client = TychoClient::new("http://localhost:4242")
TychoClient::new("http://localhost:4242").expect("Failed to create Tycho client"); .into_diagnostic()
.wrap_err("Failed to create Tycho client")?;
let chain = Chain::Ethereum; let chain = Chain::Ethereum;
let protocol_system = "test_protocol"; let protocol_system = "test_protocol";
@@ -158,7 +165,8 @@ fn validate_state(
// module, in order to simplify debugging // module, in order to simplify debugging
let protocol_components = rt let protocol_components = rt
.block_on(tycho_client.get_protocol_components(protocol_system, chain)) .block_on(tycho_client.get_protocol_components(protocol_system, chain))
.expect("Failed to get protocol components"); .into_diagnostic()
.wrap_err("Failed to get protocol components")?;
let expected_ids = expected_components let expected_ids = expected_components
.iter() .iter()
@@ -167,11 +175,13 @@ fn validate_state(
let protocol_states = rt let protocol_states = rt
.block_on(tycho_client.get_protocol_state(protocol_system, expected_ids, chain)) .block_on(tycho_client.get_protocol_state(protocol_system, expected_ids, chain))
.expect("Failed to get protocol state"); .into_diagnostic()
.wrap_err("Failed to get protocol state")?;
let vm_storages = rt let vm_storages = rt
.block_on(tycho_client.get_contract_state(protocol_system, chain)) .block_on(tycho_client.get_contract_state(protocol_system, chain))
.expect("Failed to get contract state"); .into_diagnostic()
.wrap_err("Failed to get contract state")?;
// Create a map of component IDs to components for easy lookup // Create a map of component IDs to components for easy lookup
let components_by_id: HashMap<String, ProtocolComponent> = protocol_components let components_by_id: HashMap<String, ProtocolComponent> = protocol_components
@@ -210,7 +220,11 @@ fn validate_state(
.compare(component, true); .compare(component, true);
match diff { match diff {
Some(diff) => { Some(diff) => {
panic!("Component {} does not match the expected state:\n{}", component_id, diff); return Err(miette!(
"Component {} does not match the expected state:\n{}",
component_id,
diff
));
} }
None => { None => {
info!("Component {} matches the expected state", component_id); info!("Component {} matches the expected state", component_id);
@@ -222,7 +236,9 @@ fn validate_state(
// Step 2: Validate Token Balances // Step 2: Validate Token Balances
// In this step, we validate that the token balances of the components match the values // In this step, we validate that the token balances of the components match the values
// on-chain, extracted by querying the token balances using a node. // on-chain, extracted by querying the token balances using a node.
let rpc_url = env::var("RPC_URL").expect("Missing ETH_RPC_URL in environment"); let rpc_url = env::var("RPC_URL")
.into_diagnostic()
.wrap_err("Missing ETH_RPC_URL in environment")?;
let rpc_provider = RPCProvider::new(rpc_url.to_string()); let rpc_provider = RPCProvider::new(rpc_url.to_string());
for (id, component) in components_by_id.iter() { for (id, component) in components_by_id.iter() {
@@ -280,9 +296,10 @@ fn validate_state(
let component_id = &id.clone(); let component_id = &id.clone();
let state = protocol_states_by_id let state = protocol_states_by_id
.get(component_id) .get(component_id)
.expect("Failed to get state for component") .wrap_err("Failed to get state for component")?
.clone(); .clone();
let component_with_state = ComponentWithState { state, component, component_tvl: None, entrypoints: vec![] }; // TODO let component_with_state =
ComponentWithState { state, component, component_tvl: None, entrypoints: vec![] }; // TODO
states.insert(component_id.clone(), component_with_state); states.insert(component_id.clone(), component_with_state);
} }
let vm_storage: HashMap<Bytes, ResponseAccount> = vm_storages let vm_storage: HashMap<Bytes, ResponseAccount> = vm_storages
@@ -311,7 +328,8 @@ fn validate_state(
let all_tokens = rt let all_tokens = rt
.block_on(tycho_client.get_tokens(Chain::Ethereum, None, None)) .block_on(tycho_client.get_tokens(Chain::Ethereum, None, None))
.expect("Failed to get tokens"); .into_diagnostic()
.wrap_err("Failed to get tokens")?;
info!("Loaded {} tokens", all_tokens.len()); info!("Loaded {} tokens", all_tokens.len());
rt.block_on(decoder.set_tokens(all_tokens)); rt.block_on(decoder.set_tokens(all_tokens));
@@ -322,7 +340,8 @@ fn validate_state(
let block_msg = rt let block_msg = rt
.block_on(decoder.decode(message)) .block_on(decoder.decode(message))
.expect("Failed to decode message"); .into_diagnostic()
.wrap_err("Failed to decode message")?;
for (id, comp) in block_msg.new_pairs.iter() { for (id, comp) in block_msg.new_pairs.iter() {
pairs pairs
@@ -333,8 +352,8 @@ fn validate_state(
// This is where we get blocked. Currently, Tycho Simulation expects the runtime to be // This is where we get blocked. Currently, Tycho Simulation expects the runtime to be
// prebuild and accessible from TychoSim - we should allow passing it when parsing the block // prebuild and accessible from TychoSim - we should allow passing it when parsing the block
// TODO: Since we don't have balances on the VM State, we could try to use Limits, otherwise ask the user // TODO: Since we don't have balances on the VM State, we could try to use Limits, otherwise ask
// to specify a set of values on the YAML file. // the user to specify a set of values on the YAML file.
for (id, state) in block_msg.states.iter() { for (id, state) in block_msg.states.iter() {
if let Some(tokens) = pairs.get(id) { if let Some(tokens) = pairs.get(id) {
let formatted_token_str = format!("{:}/{:}", &tokens[0].symbol, &tokens[1].symbol); let formatted_token_str = format!("{:}/{:}", &tokens[0].symbol, &tokens[1].symbol);
@@ -358,4 +377,5 @@ fn validate_state(
// e)) .ok(); // e)) .ok();
} }
} }
Ok(())
} }

View File

@@ -7,6 +7,7 @@ use std::{
}; };
use dotenv::dotenv; use dotenv::dotenv;
use miette::{miette, IntoDiagnostic, WrapErr};
use tracing::debug; use tracing::debug;
use crate::config::ProtocolComponentWithTestConfig; use crate::config::ProtocolComponentWithTestConfig;
@@ -30,7 +31,7 @@ impl TychoRunner {
start_block: u64, start_block: u64,
end_block: u64, end_block: u64,
protocol_type_names: &[String], protocol_type_names: &[String],
) -> Result<(), Box<dyn std::error::Error>> { ) -> miette::Result<()> {
// Expects a .env present in the same folder as package root (where Cargo.toml is) // Expects a .env present in the same folder as package root (where Cargo.toml is)
dotenv().ok(); dotenv().ok();
@@ -67,28 +68,21 @@ impl TychoRunner {
cmd.stdout(Stdio::piped()) cmd.stdout(Stdio::piped())
.stderr(Stdio::piped()); .stderr(Stdio::piped());
let mut process = match cmd.spawn() { let mut process = cmd
Ok(p) => p, .spawn()
Err(e) => { .into_diagnostic()
println!("Error running Tycho indexer: {}", e); .wrap_err("Error running Tycho indexer")?;
return Err(e.into());
}
};
if self.with_binary_logs { if self.with_binary_logs {
Self::handle_process_output(&mut process); Self::handle_process_output(&mut process);
} }
match process.wait() { let status = process
Ok(status) => { .wait()
if !status.success() { .into_diagnostic()
return Err(format!("Process exited with non-zero status: {}", status).into()); .wrap_err("Failed to wait on Tycho indexer process")?;
} if !status.success() {
} return Err(miette!("Process exited with non-zero status: {status}"));
Err(e) => {
println!("Error waiting for Tycho indexer: {}", e);
return Err(e.into());
}
} }
Ok(()) Ok(())
@@ -101,7 +95,7 @@ impl TychoRunner {
start_block: u64, start_block: u64,
stop_block: u64, stop_block: u64,
skip_balance_check: bool, skip_balance_check: bool,
) -> R ) -> miette::Result<R>
where where
F: FnOnce(&Vec<ProtocolComponentWithTestConfig>, u64, u64, bool) -> R, F: FnOnce(&Vec<ProtocolComponentWithTestConfig>, u64, u64, bool) -> R,
{ {
@@ -152,7 +146,7 @@ impl TychoRunner {
eprintln!("Failed to join RPC thread"); eprintln!("Failed to join RPC thread");
} }
result Ok(result)
} }
// Helper method to handle process output in separate threads // Helper method to handle process output in separate threads

View File

@@ -1,5 +1,4 @@
use std::{ use std::{
error::Error,
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
@@ -10,16 +9,17 @@ use figment::{
value::Value, value::Value,
Figment, Figment,
}; };
use miette::IntoDiagnostic;
use tracing::error; use tracing::error;
/// Build a Substreams package with modifications to the YAML file. /// Build a Substreams package with modifications to the YAML file.
pub fn build_spkg(yaml_file_path: &PathBuf, initial_block: u64) -> Result<String, Box<dyn Error>> { pub fn build_spkg(yaml_file_path: &PathBuf, initial_block: u64) -> miette::Result<String> {
// Create a backup file of the unmodified Substreams protocol YAML config file. // Create a backup file of the unmodified Substreams protocol YAML config file.
let backup_file_path = yaml_file_path.with_extension("backup"); let backup_file_path = yaml_file_path.with_extension("backup");
fs::copy(yaml_file_path, &backup_file_path)?; fs::copy(yaml_file_path, &backup_file_path).into_diagnostic()?;
let figment = Figment::new().merge(Yaml::file(yaml_file_path)); let figment = Figment::new().merge(Yaml::file(yaml_file_path));
let mut data: Value = figment.extract()?; let mut data: Value = figment.extract().into_diagnostic()?;
// Apply the modification function to update the YAML files // Apply the modification function to update the YAML files
modify_initial_block(&mut data, initial_block); modify_initial_block(&mut data, initial_block);
@@ -52,8 +52,8 @@ pub fn build_spkg(yaml_file_path: &PathBuf, initial_block: u64) -> Result<String
let spkg_name = format!("{}/{}-{}.spkg", parent_dir, package_name, package_version); let spkg_name = format!("{}/{}-{}.spkg", parent_dir, package_name, package_version);
// Write the modified YAML back to the file // Write the modified YAML back to the file
let yaml_string = serde_yaml::to_string(&data)?; let yaml_string = serde_yaml::to_string(&data).into_diagnostic()?;
fs::write(yaml_file_path, yaml_string)?; fs::write(yaml_file_path, yaml_string).into_diagnostic()?;
// Run the substreams pack command to create the spkg // Run the substreams pack command to create the spkg
// WARNING: Ensure substreams is in the PATH // WARNING: Ensure substreams is in the PATH
@@ -71,15 +71,18 @@ pub fn build_spkg(yaml_file_path: &PathBuf, initial_block: u64) -> Result<String
} }
} }
Err(e) => { Err(e) => {
error!("Error running substreams pack command: {}. \ error!(
"Error running substreams pack command: {}. \
Ensure that the wasm target was built and that substreams CLI\ Ensure that the wasm target was built and that substreams CLI\
is installed and exported on PATH", e); is installed and exported on PATH",
e
);
} }
} }
// Restore the original YAML from backup // Restore the original YAML from backup
fs::copy(&backup_file_path, yaml_file_path)?; fs::copy(&backup_file_path, yaml_file_path).into_diagnostic()?;
fs::remove_file(&backup_file_path)?; fs::remove_file(&backup_file_path).into_diagnostic()?;
Ok(spkg_name) Ok(spkg_name)
} }