From 2afd2127479f5518272d8ce138db21faf4bb390a Mon Sep 17 00:00:00 2001 From: ksew1 <95349104+ksew1@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:58:58 +0000 Subject: [PATCH] Add backtrace to forge (#2679) Closes https://github.com/foundry-rs/starknet-foundry/issues/2468 ## Introduced changes - Forge will display backtrace for contracts in format similar to rust ## Checklist - [x] Linked relevant issue - [x] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [x] Added changes to `CHANGELOG.md` --- .github/workflows/ci.yml | 5 +- Cargo.lock | 1 + .../execution/cairo1_execution.rs | 22 +- .../execution/deprecated/cairo0_execution.rs | 15 +- .../execution/entry_point.rs | 78 +++++- crates/cheatnet/src/state.rs | 8 + crates/forge-runner/Cargo.toml | 1 + crates/forge-runner/src/backtrace.rs | 226 ++++++++++++++++++ crates/forge-runner/src/lib.rs | 1 + crates/forge-runner/src/running.rs | 25 +- crates/forge-runner/src/test_case_summary.rs | 7 +- .../tests/data/backtrace_panic/Scarb.toml | 22 ++ .../tests/data/backtrace_panic/src/lib.cairo | 61 +++++ .../tests/data/backtrace_vm_error/Scarb.toml | 22 ++ .../data/backtrace_vm_error/src/lib.cairo | 68 ++++++ crates/forge/tests/e2e/backtrace.rs | 126 ++++++++++ crates/forge/tests/e2e/mod.rs | 1 + 17 files changed, 664 insertions(+), 25 deletions(-) create mode 100644 crates/forge-runner/src/backtrace.rs create mode 100644 crates/forge/tests/data/backtrace_panic/Scarb.toml create mode 100644 crates/forge/tests/data/backtrace_panic/src/lib.cairo create mode 100644 crates/forge/tests/data/backtrace_vm_error/Scarb.toml create mode 100644 crates/forge/tests/data/backtrace_vm_error/src/lib.cairo create mode 100644 crates/forge/tests/e2e/backtrace.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1605611422..9d10c834bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,8 +88,8 @@ jobs: - name: nextest partition ${{ matrix.partition }}/8 run: cargo nextest run --partition 'count:${{ matrix.partition }}/8' --archive-file 'nextest-archive.tar.zst' e2e - test-coverage: - name: Test coverage + test-scarb-2-8-3: + name: Test scarb 2.8.3 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -105,6 +105,7 @@ jobs: curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh - run: cargo test --package forge --features scarb_2_8_3 --test main e2e::coverage + - run: cargo test --package forge --features scarb_2_8_3 --test main e2e::backtrace test-coverage-error: name: Test coverage error diff --git a/Cargo.lock b/Cargo.lock index bb7c4f1f20..2a6dd09c6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2226,6 +2226,7 @@ dependencies = [ "cairo-lang-sierra-to-casm", "cairo-lang-sierra-type-size", "cairo-lang-starknet", + "cairo-lang-starknet-classes", "cairo-lang-test-plugin", "cairo-lang-utils", "cairo-vm", diff --git a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/cairo1_execution.rs b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/cairo1_execution.rs index 22e656fc02..4b63a11d48 100644 --- a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/cairo1_execution.rs +++ b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/cairo1_execution.rs @@ -1,22 +1,23 @@ +use crate::runtime_extensions::call_to_blockifier_runtime_extension::execution::entry_point::{ + ContractClassEntryPointExecutionResult, EntryPointExecutionErrorWithLastPc, GetLastPc, + OnErrorLastPc, +}; use crate::runtime_extensions::call_to_blockifier_runtime_extension::CheatnetState; use crate::runtime_extensions::cheatable_starknet_runtime_extension::CheatableStarknetRuntimeExtension; use crate::runtime_extensions::common::get_relocated_vm_trace; -use blockifier::execution::call_info::CallInfo; -use blockifier::execution::deprecated_syscalls::hint_processor::SyscallCounter; use blockifier::execution::entry_point_execution::{ finalize_execution, initialize_execution_context, prepare_call_arguments, VmExecutionContext, }; use blockifier::{ execution::{ contract_class::{ContractClassV1, EntryPointV1}, - entry_point::{CallEntryPoint, EntryPointExecutionContext, EntryPointExecutionResult}, + entry_point::{CallEntryPoint, EntryPointExecutionContext}, errors::EntryPointExecutionError, execution_utils::Args, }, state::state_api::State, }; use cairo_vm::vm::errors::cairo_run_errors::CairoRunError; -use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry; use cairo_vm::{ hint_processor::hint_processor_definition::HintProcessor, vm::runners::cairo_runner::{CairoArg, CairoRunner, ExecutionResources}, @@ -31,7 +32,7 @@ pub fn execute_entry_point_call_cairo1( cheatnet_state: &mut CheatnetState, // Added parameter resources: &mut ExecutionResources, context: &mut EntryPointExecutionContext, -) -> EntryPointExecutionResult<(CallInfo, SyscallCounter, Option>)> { +) -> ContractClassEntryPointExecutionResult { let VmExecutionContext { mut runner, mut syscall_handler, @@ -68,7 +69,8 @@ pub fn execute_entry_point_call_cairo1( &entry_point, &args, program_extra_data_length, - )?; + ) + .on_error_get_last_pc(&mut runner)?; let vm_trace = if cheatable_runtime .extension @@ -86,6 +88,7 @@ pub fn execute_entry_point_call_cairo1( .syscall_counter .clone(); + let last_pc = runner.get_last_pc(); let call_info = finalize_execution( runner, cheatable_runtime.extended_runtime.hint_handler, @@ -94,8 +97,11 @@ pub fn execute_entry_point_call_cairo1( program_extra_data_length, )?; if call_info.execution.failed { - return Err(EntryPointExecutionError::ExecutionFailed { - error_data: call_info.execution.retdata.0, + return Err(EntryPointExecutionErrorWithLastPc { + source: EntryPointExecutionError::ExecutionFailed { + error_data: call_info.execution.retdata.0, + }, + last_pc, }); } diff --git a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/deprecated/cairo0_execution.rs b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/deprecated/cairo0_execution.rs index 35c139c502..8a1e89cbcf 100644 --- a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/deprecated/cairo0_execution.rs +++ b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/deprecated/cairo0_execution.rs @@ -1,23 +1,21 @@ +use crate::runtime_extensions::call_to_blockifier_runtime_extension::execution::entry_point::{ + ContractClassEntryPointExecutionResult, OnErrorLastPc, +}; use crate::runtime_extensions::call_to_blockifier_runtime_extension::CheatnetState; use crate::runtime_extensions::deprecated_cheatable_starknet_extension::runtime::{ DeprecatedExtendedRuntime, DeprecatedStarknetRuntime, }; use crate::runtime_extensions::deprecated_cheatable_starknet_extension::DeprecatedCheatableStarknetRuntimeExtension; -use blockifier::execution::call_info::CallInfo; use blockifier::execution::contract_class::ContractClassV0; use blockifier::execution::deprecated_entry_point_execution::{ finalize_execution, initialize_execution_context, prepare_call_arguments, VmExecutionContext, }; -use blockifier::execution::entry_point::{ - CallEntryPoint, EntryPointExecutionContext, EntryPointExecutionResult, -}; +use blockifier::execution::entry_point::{CallEntryPoint, EntryPointExecutionContext}; use blockifier::execution::errors::EntryPointExecutionError; use blockifier::execution::execution_utils::Args; -use blockifier::execution::syscalls::hint_processor::SyscallCounter; use blockifier::state::state_api::State; use cairo_vm::hint_processor::hint_processor_definition::HintProcessor; use cairo_vm::vm::runners::cairo_runner::{CairoArg, CairoRunner, ExecutionResources}; -use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry; // blockifier/src/execution/deprecated_execution.rs:36 (execute_entry_point_call) pub fn execute_entry_point_call_cairo0( @@ -27,7 +25,7 @@ pub fn execute_entry_point_call_cairo0( cheatnet_state: &mut CheatnetState, resources: &mut ExecutionResources, context: &mut EntryPointExecutionContext, -) -> EntryPointExecutionResult<(CallInfo, SyscallCounter, Option>)> { +) -> ContractClassEntryPointExecutionResult { let VmExecutionContext { mut runner, mut syscall_handler, @@ -61,7 +59,8 @@ pub fn execute_entry_point_call_cairo0( &mut cheatable_syscall_handler, entry_point_pc, &args, - )?; + ) + .on_error_get_last_pc(&mut runner)?; let syscall_counter = cheatable_syscall_handler .extended_runtime diff --git a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/entry_point.rs b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/entry_point.rs index 87f494dd6d..5aeed3dcb6 100644 --- a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/entry_point.rs +++ b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/entry_point.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use super::cairo1_execution::execute_entry_point_call_cairo1; use crate::runtime_extensions::call_to_blockifier_runtime_extension::execution::deprecated::cairo0_execution::execute_entry_point_call_cairo0; use crate::runtime_extensions::call_to_blockifier_runtime_extension::CheatnetState; -use crate::state::{CallTrace, CallTraceNode, CheatStatus}; +use crate::state::{CallTrace, CallTraceNode, CheatStatus, EncounteredError}; use blockifier::execution::call_info::{CallExecution, Retdata}; use blockifier::{ execution::{ @@ -17,7 +17,7 @@ use blockifier::{ }, state::state_api::State, }; -use cairo_vm::vm::runners::cairo_runner::ExecutionResources; +use cairo_vm::vm::runners::cairo_runner::{CairoRunner, ExecutionResources}; use starknet_api::{ core::ClassHash, deprecated_contract_class::EntryPointType, @@ -28,11 +28,17 @@ use std::rc::Rc; use blockifier::execution::deprecated_syscalls::hint_processor::SyscallCounter; use starknet_types_core::felt::Felt; use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry; +use thiserror::Error; use conversions::FromConv; use crate::runtime_extensions::call_to_blockifier_runtime_extension::rpc::{AddressOrClassHash, CallResult}; use crate::runtime_extensions::common::sum_syscall_counters; use conversions::string::TryFromHexStr; +pub(crate) type ContractClassEntryPointExecutionResult = Result< + (CallInfo, SyscallCounter, Option>), + EntryPointExecutionErrorWithLastPc, +>; + // blockifier/src/execution/entry_point.rs:180 (CallEntryPoint::execute) #[allow(clippy::too_many_lines)] pub fn execute_call_entry_point( @@ -151,7 +157,15 @@ pub fn execute_call_entry_point( ); Ok(call_info) } - Err(err) => { + Err(EntryPointExecutionErrorWithLastPc { + source: err, + last_pc: pc, + }) => { + if let Some(pc) = pc { + cheatnet_state + .encountered_errors + .push(EncounteredError { pc, class_hash }); + } exit_error_call(&err, cheatnet_state, resources, entry_point); Err(err) } @@ -297,3 +311,61 @@ fn aggregate_syscall_counters(trace: &Rc>) -> SyscallCounter } result } + +#[derive(Debug, Error)] +#[error("{}", source)] +pub struct EntryPointExecutionErrorWithLastPc { + pub source: EntryPointExecutionError, + pub last_pc: Option, +} + +impl From for EntryPointExecutionErrorWithLastPc +where + T: Into, +{ + fn from(value: T) -> Self { + Self { + source: value.into(), + last_pc: None, + } + } +} + +pub(crate) trait OnErrorLastPc: Sized { + fn on_error_get_last_pc( + self, + runner: &mut CairoRunner, + ) -> Result; +} + +impl OnErrorLastPc for Result { + fn on_error_get_last_pc( + self, + runner: &mut CairoRunner, + ) -> Result { + match self { + Err(source) => { + let last_pc = runner.get_last_pc(); + + Err(EntryPointExecutionErrorWithLastPc { source, last_pc }) + } + Ok(value) => Ok(value), + } + } +} + +pub trait GetLastPc { + fn get_last_pc(&mut self) -> Option; +} + +impl GetLastPc for CairoRunner { + fn get_last_pc(&mut self) -> Option { + if self.relocated_trace.is_none() { + self.relocate(true).ok()?; + } + self.relocated_trace + .as_ref() + .and_then(|trace| trace.last()) + .map(|entry| entry.pc) + } +} diff --git a/crates/cheatnet/src/state.rs b/crates/cheatnet/src/state.rs index a557ecd694..5b0fbf7189 100644 --- a/crates/cheatnet/src/state.rs +++ b/crates/cheatnet/src/state.rs @@ -320,6 +320,12 @@ pub struct TraceData { pub is_vm_trace_needed: bool, } +#[derive(Clone)] +pub struct EncounteredError { + pub pc: usize, + pub class_hash: ClassHash, +} + pub struct CheatnetState { pub cheated_execution_info_contracts: HashMap, pub global_cheated_execution_info: ExecutionInfoMock, @@ -332,6 +338,7 @@ pub struct CheatnetState { pub deploy_salt_base: u32, pub block_info: BlockInfo, pub trace_data: TraceData, + pub encountered_errors: Vec, } impl Default for CheatnetState { @@ -357,6 +364,7 @@ impl Default for CheatnetState { current_call_stack: NotEmptyCallStack::from(test_call), is_vm_trace_needed: false, }, + encountered_errors: vec![], } } } diff --git a/crates/forge-runner/Cargo.toml b/crates/forge-runner/Cargo.toml index 929c0f720f..a9e35b7012 100644 --- a/crates/forge-runner/Cargo.toml +++ b/crates/forge-runner/Cargo.toml @@ -19,6 +19,7 @@ cairo-lang-sierra-type-size.workspace = true cairo-lang-sierra-gas.workspace = true cairo-lang-sierra-ap-change.workspace = true cairo-lang-test-plugin.workspace = true +cairo-lang-starknet-classes.workspace = true cairo-annotations.workspace = true starknet-types-core.workspace = true starknet_api.workspace = true diff --git a/crates/forge-runner/src/backtrace.rs b/crates/forge-runner/src/backtrace.rs new file mode 100644 index 0000000000..7afad162bb --- /dev/null +++ b/crates/forge-runner/src/backtrace.rs @@ -0,0 +1,226 @@ +use anyhow::{Context, Result}; +use cairo_annotations::annotations::coverage::{ + CodeLocation, ColumnNumber, CoverageAnnotationsV1, LineNumber, VersionedCoverageAnnotations, +}; +use cairo_annotations::annotations::profiler::{ + FunctionName, ProfilerAnnotationsV1, VersionedProfilerAnnotations, +}; +use cairo_annotations::annotations::TryFromDebugInfo; +use cairo_lang_sierra::program::StatementIdx; +use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass; +use cairo_lang_starknet_classes::contract_class::ContractClass; +use cheatnet::runtime_extensions::forge_runtime_extension::contracts_data::ContractsData; +use cheatnet::state::EncounteredError; +use indoc::indoc; +use itertools::Itertools; +use rayon::prelude::*; +use starknet_api::core::ClassHash; +use std::collections::HashMap; +use std::fmt::Display; +use std::{env, fmt}; + +const BACKTRACE_ENV: &str = "SNFORGE_BACKTRACE"; + +pub fn add_backtrace_footer( + message: String, + contracts_data: &ContractsData, + encountered_errors: &[EncounteredError], +) -> String { + if encountered_errors.is_empty() { + return message; + } + + let is_backtrace_enabled = env::var(BACKTRACE_ENV).is_ok_and(|value| value == "1"); + if !is_backtrace_enabled { + return format!( + "{message}\nnote: run with `{BACKTRACE_ENV}=1` environment variable to display a backtrace", + ); + } + + BacktraceContractRepository::new(contracts_data, encountered_errors) + .map(|repository| { + encountered_errors + .iter() + .filter_map(|error| repository.get_backtrace(error.pc, error.class_hash)) + .map(|backtrace| backtrace.to_string()) + .collect::>() + .join("\n") + }) + .map_or_else( + |err| format!("{message}\nfailed to create backtrace: {err}"), + |backtraces| format!("{message}\n{backtraces}"), + ) +} + +struct ContractBacktraceData { + contract_name: String, + casm_debug_info_start_offsets: Vec, + coverage_annotations: CoverageAnnotationsV1, + profiler_annotations: ProfilerAnnotationsV1, +} + +impl ContractBacktraceData { + fn new(class_hash: &ClassHash, contracts_data: &ContractsData) -> Result { + let contract_name = contracts_data + .get_contract_name(class_hash) + .context(format!( + "failed to get contract name for class hash: {class_hash}" + ))? + .clone(); + + let contract_artifacts = contracts_data + .get_artifacts(&contract_name) + .context(format!( + "failed to get artifacts for contract name: {contract_name}" + ))?; + + let contract_class = serde_json::from_str::(&contract_artifacts.sierra)?; + + let sierra_debug_info = contract_class + .sierra_program_debug_info + .as_ref() + .context("debug info not found")?; + + let VersionedCoverageAnnotations::V1(coverage_annotations) = + VersionedCoverageAnnotations::try_from_debug_info(sierra_debug_info).context(indoc! { + "perhaps the contract was compiled without the following entry in Scarb.toml under [profile.dev.cairo]: + unstable-add-statements-code-locations-debug-info = true + + or scarb version is less than 2.8.0 + " + })?; + + let VersionedProfilerAnnotations::V1(profiler_annotations) = + VersionedProfilerAnnotations::try_from_debug_info(sierra_debug_info).context(indoc! { + "perhaps the contract was compiled without the following entry in Scarb.toml under [profile.dev.cairo]: + unstable-add-statements-functions-debug-info = true + + or scarb version is less than 2.8.0 + " + })?; + + // Not optimal, but USC doesn't produce debug info for the contract class + let (_, debug_info) = CasmContractClass::from_contract_class_with_debug_info( + contract_class, + true, + usize::MAX, + )?; + + let casm_debug_info_start_offsets = debug_info + .sierra_statement_info + .iter() + .map(|statement_debug_info| statement_debug_info.start_offset) + .collect(); + + Ok(Self { + contract_name, + casm_debug_info_start_offsets, + coverage_annotations, + profiler_annotations, + }) + } + + fn backtrace_from(&self, pc: usize) -> Option { + let sierra_statement_idx = StatementIdx( + self.casm_debug_info_start_offsets + .partition_point(|start_offset| *start_offset < pc - 1) + .saturating_sub(1), + ); + + let code_locations = self + .coverage_annotations + .statements_code_locations + .get(&sierra_statement_idx)?; + + let function_names = self + .profiler_annotations + .statements_functions + .get(&sierra_statement_idx)?; + + let stack = code_locations + .iter() + .zip(function_names) + .map(|(code_location, function_name)| Backtrace { + code_location, + function_name, + }) + .collect(); + + Some(BacktraceStack { + pc, + contract_name: &self.contract_name, + stack, + }) + } +} + +struct BacktraceContractRepository(HashMap); + +impl BacktraceContractRepository { + fn new( + contracts_data: &ContractsData, + encountered_errors: &[EncounteredError], + ) -> Result { + Ok(Self( + encountered_errors + .iter() + .map(|error| error.class_hash) + .unique() + .collect::>() + .par_iter() + .map(|class_hash| { + ContractBacktraceData::new(class_hash, contracts_data) + .map(|contract_data| (*class_hash, contract_data)) + }) + .collect::>()?, + )) + } + + fn get_backtrace(&self, pc: usize, class_hash: ClassHash) -> Option { + self.0.get(&class_hash)?.backtrace_from(pc) + } +} + +struct Backtrace<'a> { + code_location: &'a CodeLocation, + function_name: &'a FunctionName, +} + +struct BacktraceStack<'a> { + pc: usize, + contract_name: &'a str, + stack: Vec>, +} + +impl Display for Backtrace<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let function_name = &self.function_name.0; + let path = truncate_at_char(&self.code_location.0 .0, '['); + let line = self.code_location.1.start.line + LineNumber(1); // most editors start line numbers from 1 + let col = self.code_location.1.start.col + ColumnNumber(1); // most editors start column numbers from 1 + + write!(f, "{function_name}\n at {path}:{line}:{col}",) + } +} + +impl Display for BacktraceStack<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!( + f, + "error occurred in contract '{}' at pc: '{}'", + self.contract_name, self.pc + )?; + writeln!(f, "stack backtrace:")?; + for (i, backtrace) in self.stack.iter().enumerate() { + writeln!(f, " {i}: {backtrace}")?; + } + Ok(()) + } +} + +fn truncate_at_char(input: &str, delimiter: char) -> &str { + match input.find(delimiter) { + Some(index) => &input[..index], + None => input, + } +} diff --git a/crates/forge-runner/src/lib.rs b/crates/forge-runner/src/lib.rs index 1c231eea19..8608f685e6 100644 --- a/crates/forge-runner/src/lib.rs +++ b/crates/forge-runner/src/lib.rs @@ -33,6 +33,7 @@ pub mod profiler_api; pub mod test_case_summary; pub mod test_target_summary; +mod backtrace; mod fuzzer; mod gas; pub mod printing; diff --git a/crates/forge-runner/src/running.rs b/crates/forge-runner/src/running.rs index d286a70bf3..21081c16c5 100644 --- a/crates/forge-runner/src/running.rs +++ b/crates/forge-runner/src/running.rs @@ -1,3 +1,4 @@ +use crate::backtrace::add_backtrace_footer; use crate::build_trace_data::test_sierra_program_path::VersionedProgramPath; use crate::forge_config::{RuntimeConfig, TestRunnerConfig}; use crate::gas::calculate_used_gas; @@ -21,7 +22,9 @@ use cheatnet::runtime_extensions::forge_runtime_extension::{ get_all_used_resources, update_top_call_execution_resources, update_top_call_l1_resources, update_top_call_vm_trace, ForgeExtension, ForgeRuntime, }; -use cheatnet::state::{BlockInfoReader, CallTrace, CheatnetState, ExtendedStateReader}; +use cheatnet::state::{ + BlockInfoReader, CallTrace, CheatnetState, EncounteredError, ExtendedStateReader, +}; use entry_code::create_entry_code; use hints::{hints_by_representation, hints_to_params}; use runtime::starknet::context::{build_context, set_max_steps}; @@ -129,6 +132,7 @@ pub struct RunResultWithInfo { pub(crate) call_trace: Rc>, pub(crate) gas_used: u128, pub(crate) used_resources: UsedResources, + pub(crate) encountered_errors: Vec, } #[allow(clippy::too_many_lines)] @@ -246,6 +250,14 @@ pub fn run_test_case( Err(err) => Err(err), }; + let encountered_errors = forge_runtime + .extended_runtime + .extended_runtime + .extension + .cheatnet_state + .encountered_errors + .clone(); + let call_trace_ref = get_call_trace_ref(&mut forge_runtime); update_top_call_execution_resources(&mut forge_runtime); @@ -269,6 +281,7 @@ pub fn run_test_case( gas_used: gas, used_resources, call_trace: call_trace_ref, + encountered_errors, }) } @@ -289,6 +302,7 @@ fn extract_test_case_summary( result_with_info.gas_used, result_with_info.used_resources, &result_with_info.call_trace, + &result_with_info.encountered_errors, contracts_data, maybe_versioned_program_path, ), @@ -298,7 +312,14 @@ fn extract_test_case_summary( msg: Some(format!( "\n {}\n", error.to_string().replace(" Custom Hint Error: ", "\n ") - )), + )) + .map(|msg| { + add_backtrace_footer( + msg, + contracts_data, + &result_with_info.encountered_errors, + ) + }), arguments: args, test_statistics: (), }, diff --git a/crates/forge-runner/src/test_case_summary.rs b/crates/forge-runner/src/test_case_summary.rs index 78340772c9..7e13bc4cc8 100644 --- a/crates/forge-runner/src/test_case_summary.rs +++ b/crates/forge-runner/src/test_case_summary.rs @@ -1,3 +1,4 @@ +use crate::backtrace::add_backtrace_footer; use crate::build_trace_data::build_profiler_call_trace; use crate::build_trace_data::test_sierra_program_path::VersionedProgramPath; use crate::expected_result::{ExpectedPanicValue, ExpectedTestResult}; @@ -8,7 +9,7 @@ use cairo_lang_runner::short_string::as_cairo_short_string; use cairo_lang_runner::{RunResult, RunResultValue}; use cheatnet::runtime_extensions::call_to_blockifier_runtime_extension::rpc::UsedResources; use cheatnet::runtime_extensions::forge_runtime_extension::contracts_data::ContractsData; -use cheatnet::state::CallTrace as InternalCallTrace; +use cheatnet::state::{CallTrace as InternalCallTrace, EncounteredError}; use conversions::byte_array::ByteArray; use num_traits::Pow; use shared::utils::build_readable_text; @@ -216,11 +217,13 @@ impl TestCaseSummary { gas: u128, used_resources: UsedResources, call_trace: &Rc>, + encountered_errors: &[EncounteredError], contracts_data: &ContractsData, maybe_versioned_program_path: &Option, ) -> Self { let name = test_case.name.clone(); - let msg = extract_result_data(&run_result, &test_case.config.expected_result); + let msg = extract_result_data(&run_result, &test_case.config.expected_result) + .map(|msg| add_backtrace_footer(msg, contracts_data, encountered_errors)); match run_result.value { RunResultValue::Success(_) => match &test_case.config.expected_result { ExpectedTestResult::Success => { diff --git a/crates/forge/tests/data/backtrace_panic/Scarb.toml b/crates/forge/tests/data/backtrace_panic/Scarb.toml new file mode 100644 index 0000000000..db687cf174 --- /dev/null +++ b/crates/forge/tests/data/backtrace_panic/Scarb.toml @@ -0,0 +1,22 @@ +[package] +name = "backtrace_panic" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.7.0" + +[dev-dependencies] +snforge_std = { path = "../../../../../snforge_std" } + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[profile.dev.cairo] +unstable-add-statements-functions-debug-info = true +unstable-add-statements-code-locations-debug-info = true diff --git a/crates/forge/tests/data/backtrace_panic/src/lib.cairo b/crates/forge/tests/data/backtrace_panic/src/lib.cairo new file mode 100644 index 0000000000..5517a8fd35 --- /dev/null +++ b/crates/forge/tests/data/backtrace_panic/src/lib.cairo @@ -0,0 +1,61 @@ +#[starknet::interface] +pub trait IOuterContract { + fn outer(self: @TState, contract_address: starknet::ContractAddress); +} + +#[starknet::contract] +pub mod OuterContract { + use super::{IInnerContractDispatcher, IInnerContractDispatcherTrait}; + + #[storage] + pub struct Storage {} + + #[abi(embed_v0)] + impl OuterContract of super::IOuterContract { + fn outer(self: @ContractState, contract_address: starknet::ContractAddress) { + let dispatcher = IInnerContractDispatcher { contract_address }; + dispatcher.inner(); + } + } +} + +#[starknet::interface] +pub trait IInnerContract { + fn inner(self: @TState); +} + +#[starknet::contract] +pub mod InnerContract { + #[storage] + pub struct Storage {} + + #[abi(embed_v0)] + impl InnerContract of super::IInnerContract { + fn inner(self: @ContractState) { + inner_call() + } + } + + fn inner_call() { + assert(1 != 1, 'aaaa'); + } +} + +#[cfg(test)] +mod Test { + use snforge_std::cheatcodes::contract_class::DeclareResultTrait; + use snforge_std::{ContractClassTrait, declare}; + use super::{IOuterContractDispatcher, IOuterContractDispatcherTrait}; + + #[test] + fn test_unwrapped_call_contract_syscall() { + let contract_inner = declare("InnerContract").unwrap().contract_class(); + let (contract_address_inner, _) = contract_inner.deploy(@array![]).unwrap(); + + let contract_outer = declare("OuterContract").unwrap().contract_class(); + let (contract_address_outer, _) = contract_outer.deploy(@array![]).unwrap(); + + let dispatcher = IOuterContractDispatcher { contract_address: contract_address_outer }; + dispatcher.outer(contract_address_inner); + } +} diff --git a/crates/forge/tests/data/backtrace_vm_error/Scarb.toml b/crates/forge/tests/data/backtrace_vm_error/Scarb.toml new file mode 100644 index 0000000000..942939e25e --- /dev/null +++ b/crates/forge/tests/data/backtrace_vm_error/Scarb.toml @@ -0,0 +1,22 @@ +[package] +name = "backtrace_vm_error" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.7.0" + +[dev-dependencies] +snforge_std = { path = "../../../../../snforge_std" } + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[profile.dev.cairo] +unstable-add-statements-functions-debug-info = true +unstable-add-statements-code-locations-debug-info = true diff --git a/crates/forge/tests/data/backtrace_vm_error/src/lib.cairo b/crates/forge/tests/data/backtrace_vm_error/src/lib.cairo new file mode 100644 index 0000000000..ec8559e832 --- /dev/null +++ b/crates/forge/tests/data/backtrace_vm_error/src/lib.cairo @@ -0,0 +1,68 @@ +#[starknet::interface] +pub trait IOuterContract { + fn outer(self: @TState, contract_address: starknet::ContractAddress); +} + +#[starknet::contract] +pub mod OuterContract { + use super::{IInnerContractDispatcher, IInnerContractDispatcherTrait}; + + #[storage] + pub struct Storage {} + + #[abi(embed_v0)] + impl OuterContract of super::IOuterContract { + fn outer(self: @ContractState, contract_address: starknet::ContractAddress) { + let dispatcher = IInnerContractDispatcher { contract_address }; + dispatcher.inner(); + } + } +} + +#[starknet::interface] +pub trait IInnerContract { + fn inner(self: @TState); +} + +#[starknet::contract] +pub mod InnerContract { + use starknet::SyscallResultTrait; + use starknet::syscalls::call_contract_syscall; + + #[storage] + pub struct Storage {} + + #[abi(embed_v0)] + impl InnerContract of super::IInnerContract { + fn inner(self: @ContractState) { + inner_call() + } + } + + fn inner_call() { + let this = starknet::get_contract_address(); + let selector = selector!("nonexistent"); + let calldata = array![].span(); + + call_contract_syscall(this, selector, calldata).unwrap_syscall(); + } +} + +#[cfg(test)] +mod Test { + use snforge_std::cheatcodes::contract_class::DeclareResultTrait; + use snforge_std::{ContractClassTrait, declare}; + use super::{IOuterContractDispatcher, IOuterContractDispatcherTrait}; + + #[test] + fn test_unwrapped_call_contract_syscall() { + let contract_inner = declare("InnerContract").unwrap().contract_class(); + let (contract_address_inner, _) = contract_inner.deploy(@array![]).unwrap(); + + let contract_outer = declare("OuterContract").unwrap().contract_class(); + let (contract_address_outer, _) = contract_outer.deploy(@array![]).unwrap(); + + let dispatcher = IOuterContractDispatcher { contract_address: contract_address_outer }; + dispatcher.outer(contract_address_inner); + } +} diff --git a/crates/forge/tests/e2e/backtrace.rs b/crates/forge/tests/e2e/backtrace.rs new file mode 100644 index 0000000000..f591ea383d --- /dev/null +++ b/crates/forge/tests/e2e/backtrace.rs @@ -0,0 +1,126 @@ +use super::common::runner::{setup_package, test_runner}; +use assert_fs::fixture::{FileWriteStr, PathChild}; +use indoc::indoc; +use shared::test_utils::output_assert::assert_stdout_contains; +use std::fs; +use toml_edit::{value, DocumentMut}; + +#[test] +#[cfg_attr(not(feature = "scarb_2_8_3"), ignore)] +fn test_backtrace_missing_env() { + let temp = setup_package("backtrace_vm_error"); + + let output = test_runner(&temp).assert().failure(); + + assert_stdout_contains( + output, + indoc! { + "Failure data: + (0x454e545259504f494e545f4e4f545f464f554e44 ('ENTRYPOINT_NOT_FOUND'), 0x454e545259504f494e545f4641494c4544 ('ENTRYPOINT_FAILED')) + note: run with `SNFORGE_BACKTRACE=1` environment variable to display a backtrace" + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "scarb_2_8_3"), ignore)] +fn test_backtrace() { + let temp = setup_package("backtrace_vm_error"); + + let output = test_runner(&temp) + .env("SNFORGE_BACKTRACE", "1") + .assert() + .failure(); + + assert_stdout_contains( + output, + indoc! { + "Failure data: + (0x454e545259504f494e545f4e4f545f464f554e44 ('ENTRYPOINT_NOT_FOUND'), 0x454e545259504f494e545f4641494c4544 ('ENTRYPOINT_FAILED')) + error occurred in contract 'InnerContract' at pc: '72' + stack backtrace: + 0: backtrace_vm_error::InnerContract::inner_call + at [..]/src/lib.cairo:47:9 + 1: backtrace_vm_error::InnerContract::InnerContract::inner + at [..]/src/lib.cairo:38:13 + 2: backtrace_vm_error::InnerContract::__wrapper__InnerContract__inner + at [..]/src/lib.cairo:37:9 + + error occurred in contract 'OuterContract' at pc: '107' + stack backtrace: + 0: backtrace_vm_error::IInnerContractDispatcherImpl::inner + at [..]/src/lib.cairo:22:1 + 1: backtrace_vm_error::OuterContract::OuterContract::outer + at [..]/src/lib.cairo:17:13 + 2: backtrace_vm_error::OuterContract::__wrapper__OuterContract__outer + at [..]/src/lib.cairo:15:9" + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "scarb_2_8_3"), ignore)] +fn test_wrong_scarb_toml_configuration() { + let temp = setup_package("backtrace_vm_error"); + + let manifest_path = temp.child("Scarb.toml"); + + let mut scarb_toml = fs::read_to_string(&manifest_path) + .unwrap() + .parse::() + .unwrap(); + + scarb_toml["profile"]["dev"]["cairo"]["unstable-add-statements-code-locations-debug-info"] = + value(false); + + manifest_path.write_str(&scarb_toml.to_string()).unwrap(); + + let output = test_runner(&temp) + .env("SNFORGE_BACKTRACE", "1") + .assert() + .failure(); + + assert_stdout_contains( + output, + indoc! { + "Failure data: + (0x454e545259504f494e545f4e4f545f464f554e44 ('ENTRYPOINT_NOT_FOUND'), 0x454e545259504f494e545f4641494c4544 ('ENTRYPOINT_FAILED')) + failed to create backtrace: perhaps the contract was compiled without the following entry in Scarb.toml under [profile.dev.cairo]: + unstable-add-statements-code-locations-debug-info = true + + or scarb version is less than 2.8.0" + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "scarb_2_8_3"), ignore)] +fn test_backtrace_panic() { + let temp = setup_package("backtrace_panic"); + + let output = test_runner(&temp) + .env("SNFORGE_BACKTRACE", "1") + .assert() + .failure(); + + assert_stdout_contains( + output, + indoc! { + "Failure data: + 0x61616161 ('aaaa') + error occurred in contract 'InnerContract' at pc: '70' + stack backtrace: + 0: backtrace_panic::InnerContract::__wrapper__InnerContract__inner + at [..]/src/lib.cairo:34:9 + + error occurred in contract 'OuterContract' at pc: '107' + stack backtrace: + 0: backtrace_panic::IInnerContractDispatcherImpl::inner + at [..]/src/lib.cairo:22:1 + 1: backtrace_panic::OuterContract::OuterContract::outer + at [..]/src/lib.cairo:17:13 + 2: backtrace_panic::OuterContract::__wrapper__OuterContract__outer + at [..]/src/lib.cairo:15:9" + }, + ); +} diff --git a/crates/forge/tests/e2e/mod.rs b/crates/forge/tests/e2e/mod.rs index 9e8d060115..9fbf0d711c 100644 --- a/crates/forge/tests/e2e/mod.rs +++ b/crates/forge/tests/e2e/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod common; +mod backtrace; mod build_profile; mod build_trace_data; mod collection;