From 73b5d14ecd4c94d4a71d554e1e0f094a12819902 Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Thu, 26 Mar 2026 14:08:32 +0200 Subject: [PATCH 1/8] feat: add test_helpers module (error_utils, test_utils) behind function_runner flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create vm/src/test_helpers/ with error_utils.rs and test_utils.rs - Move from cairo_test_suite/ (fix filename typo: utlis → utils) - Fix crate:: import paths (were cairo_vm:: when outside the crate) - Fix $crate in macro_export macro (clippy::crate_in_macro_def) - Simplify load_cairo_program! path using with_file_name() - Gate module behind function_runner feature in lib.rs Co-Authored-By: Claude Sonnet 4.6 --- vm/src/lib.rs | 3 + vm/src/test_helpers/error_utils.rs | 170 +++++++++++++++++++++++++++++ vm/src/test_helpers/mod.rs | 4 + vm/src/test_helpers/test_utils.rs | 53 +++++++++ 4 files changed, 230 insertions(+) create mode 100644 vm/src/test_helpers/error_utils.rs create mode 100644 vm/src/test_helpers/mod.rs create mode 100644 vm/src/test_helpers/test_utils.rs diff --git a/vm/src/lib.rs b/vm/src/lib.rs index ab2bbc2e5f..2406f05bf1 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -26,6 +26,9 @@ pub mod types; pub mod utils; pub mod vm; +#[cfg(feature = "function_runner")] +pub mod test_helpers; + // TODO: use `Felt` directly pub use starknet_types_core::felt::Felt as Felt252; diff --git a/vm/src/test_helpers/error_utils.rs b/vm/src/test_helpers/error_utils.rs new file mode 100644 index 0000000000..51cf3b5751 --- /dev/null +++ b/vm/src/test_helpers/error_utils.rs @@ -0,0 +1,170 @@ +//! Test utilities for Cairo VM result assertions. + +use crate::vm::errors::{ + cairo_run_errors::CairoRunError, hint_errors::HintError, vm_errors::VirtualMachineError, + vm_exception::VmException, +}; + +/// Asserts VM result is `Ok` or matches an error pattern. +#[macro_export] +macro_rules! assert_vm_result { + ($res:expr, ok $(,)?) => {{ + match &$res { + Ok(_) => {} + Err(e) => panic!("Expected Ok, got Err: {:#?}", e), + } + }}; + + ($res:expr, err $pat:pat $(,)?) => {{ + match &$res { + Ok(v) => panic!("Expected Err, got Ok: {v:?}"), + Err(e) => assert!( + matches!(e, $pat), + "Unexpected error variant.\nExpected: {}\nGot: {:#?}", + stringify!($pat), + e + ), + } + }}; + + ($res:expr, err $pat:pat if $guard:expr $(,)?) => {{ + match &$res { + Ok(v) => panic!("Expected Err, got Ok: {v:?}"), + Err(e) => assert!( + matches!(e, $pat if $guard), + "Unexpected error variant.\nExpected: {} (with guard)\nGot: {:#?}", + stringify!($pat), + e + ), + } + }}; +} + +/// Type alias for check functions that validate test results. +pub type VmCheck = fn(&std::result::Result); + +/// Asserts that the result is `Ok`. +pub fn expect_ok(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!(res, ok); +} + +/// Asserts that the result is `HintError::AssertNotZero`. +pub fn expect_hint_assert_not_zero(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertNotZero(_))) + ); +} + +/// Asserts that the result is `HintError::AssertNotEqualFail`. +pub fn expect_assert_not_equal_fail(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertNotEqualFail(_))) + ); +} + +/// Asserts that the result is `HintError::Internal(VirtualMachineError::DiffTypeComparison)`. +pub fn expect_diff_type_comparison(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::Internal(VirtualMachineError::DiffTypeComparison(_)))) + ); +} + +/// Asserts that the result is `HintError::Internal(VirtualMachineError::DiffIndexComp)`. +pub fn expect_diff_index_comp(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::Internal(VirtualMachineError::DiffIndexComp(_)))) + ); +} + +/// Asserts that the result is `HintError::ValueOutside250BitRange`. +pub fn expect_hint_value_outside_250_bit_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::ValueOutside250BitRange(_))) + ); +} + +/// Asserts that the result is `HintError::NonLeFelt252`. +pub fn expect_non_le_felt252(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::NonLeFelt252(_))) + ); +} + +/// Asserts that the result is `HintError::AssertLtFelt252`. +pub fn expect_assert_lt_felt252(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertLtFelt252(_))) + ); +} + +/// Asserts that the result is `HintError::ValueOutsideValidRange`. +pub fn expect_hint_value_outside_valid_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::ValueOutsideValidRange(_))) + ); +} + +/// Asserts that the result is `HintError::OutOfValidRange`. +pub fn expect_hint_out_of_valid_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::OutOfValidRange(_))) + ); +} + +/// Asserts that the result is `HintError::SplitIntNotZero`. +pub fn expect_split_int_not_zero(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::SplitIntNotZero)) + ); +} + +/// Asserts that the result is `HintError::SplitIntLimbOutOfRange`. +pub fn expect_split_int_limb_out_of_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::SplitIntLimbOutOfRange(_))) + ); +} diff --git a/vm/src/test_helpers/mod.rs b/vm/src/test_helpers/mod.rs new file mode 100644 index 0000000000..438189d0ad --- /dev/null +++ b/vm/src/test_helpers/mod.rs @@ -0,0 +1,4 @@ +//! Test helpers for Cairo VM — enabled by the `function_runner` feature. + +pub mod error_utils; +pub mod test_utils; diff --git a/vm/src/test_helpers/test_utils.rs b/vm/src/test_helpers/test_utils.rs new file mode 100644 index 0000000000..649cda3238 --- /dev/null +++ b/vm/src/test_helpers/test_utils.rs @@ -0,0 +1,53 @@ +/// Loads a compiled Cairo `.json` program from the same directory as the calling source file. +/// +/// Pass only the filename (no directory prefix). The directory is inferred from the call site +/// via `file!()`, so the `.json` must live next to the `.cairo` and `.rs` files. +/// +/// # Example +/// ```rust +/// static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("main_math_test.json")); +/// ``` +/// +/// # Panics +/// - If the `.json` file does not exist: run `make tests_cairo_programs` first. +/// - If the `.json` file cannot be parsed as a Cairo `Program`. +#[macro_export] +macro_rules! load_cairo_program { + ($name:literal) => {{ + // CARGO_MANIFEST_DIR is the `vm/` crate dir; workspace root is one level up. + // file!() expands at the call site — with_file_name replaces the filename portion + // so the JSON is resolved relative to the calling source file's directory. + let json_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("vm crate should have a parent directory") + .join(file!()) + .with_file_name($name); + + let bytes = std::fs::read(&json_path).unwrap_or_else(|err| { + panic!( + "Cairo program not found at {json_path:?}: {err}\n\ + Did you run `make cairo_test_suite_programs`?" + ) + }); + + $crate::types::program::Program::from_bytes(&bytes, None) + .unwrap_or_else(|e| panic!("Failed to parse Cairo program at {json_path:?}: {e}")) + }}; +} + +/// Asserts that a `MaybeRelocatable` reference equals a value convertible into `MaybeRelocatable`. +#[macro_export] +macro_rules! assert_mr_eq { + ($left:expr, $right:expr) => {{ + let right_mr = ($right) + .try_into() + .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + assert_eq!($left, &right_mr); + }}; + ($left:expr, $right:expr, $($arg:tt)+) => {{ + let right_mr = ($right) + .try_into() + .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + assert_eq!($left, &right_mr, $($arg)+); + }}; +} From 61f53018b11904d2d1e39cb25c203380c0f648e5 Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Sun, 29 Mar 2026 15:54:33 +0300 Subject: [PATCH 2/8] test(test_helpers): add unit tests for assert_mr_eq!, load_cairo_program! and error_utils checkers Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + vm/src/test_helpers/dummy.json | 69 ++++++++ vm/src/test_helpers/error_utils.rs | 246 +++++++++++++++++++++++++++++ vm/src/test_helpers/test_utils.rs | 87 ++++++++++ 4 files changed, 403 insertions(+) create mode 100644 vm/src/test_helpers/dummy.json diff --git a/.gitignore b/.gitignore index 69fa82e403..8698bfa233 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ **/*.json !hint_accountant/whitelists/*.json !cairo_programs/manually_compiled/*.json +!vm/src/test_helpers/dummy.json **/*.casm **/*.sierra **/*.trace diff --git a/vm/src/test_helpers/dummy.json b/vm/src/test_helpers/dummy.json new file mode 100644 index 0000000000..4c1bf5ad81 --- /dev/null +++ b/vm/src/test_helpers/dummy.json @@ -0,0 +1,69 @@ +{ + "attributes": [], + "builtins": [], + "compiler_version": "0.13.5", + "data": [ + "0x208b7fff7fff7ffe" + ], + "debug_info": { + "file_contents": {}, + "instruction_locations": { + "0": { + "accessible_scopes": [ + "__main__", + "__main__.main" + ], + "flow_tracking_data": { + "ap_tracking": { + "group": 0, + "offset": 0 + }, + "reference_ids": {} + }, + "hints": [], + "inst": { + "end_col": 15, + "end_line": 2, + "input_file": { + "filename": "vm/src/test_helpers/dummy.cairo" + }, + "start_col": 5, + "start_line": 2 + } + } + } + }, + "hints": {}, + "identifiers": { + "__main__.main": { + "decorators": [], + "pc": 0, + "type": "function" + }, + "__main__.main.Args": { + "full_name": "__main__.main.Args", + "members": {}, + "size": 0, + "type": "struct" + }, + "__main__.main.ImplicitArgs": { + "full_name": "__main__.main.ImplicitArgs", + "members": {}, + "size": 0, + "type": "struct" + }, + "__main__.main.Return": { + "cairo_type": "()", + "type": "type_definition" + }, + "__main__.main.SIZEOF_LOCALS": { + "type": "const", + "value": 0 + } + }, + "main_scope": "__main__", + "prime": "0x800000000000011000000000000000000000000000000000000000000000001", + "reference_manager": { + "references": [] + } +} diff --git a/vm/src/test_helpers/error_utils.rs b/vm/src/test_helpers/error_utils.rs index 51cf3b5751..a4eabfbb18 100644 --- a/vm/src/test_helpers/error_utils.rs +++ b/vm/src/test_helpers/error_utils.rs @@ -168,3 +168,249 @@ pub fn expect_split_int_limb_out_of_range(res: &std::result::Result<(), CairoRun }) if matches!(boxed.as_ref(), (_, HintError::SplitIntLimbOutOfRange(_))) ); } + +#[cfg(test)] +mod tests { + use crate::{ + types::relocatable::{MaybeRelocatable, Relocatable}, + vm::errors::{ + cairo_run_errors::CairoRunError, hint_errors::HintError, + vm_errors::VirtualMachineError, vm_exception::VmException, + }, + }; + + use super::*; + + /// Wraps a `HintError` in the full `CairoRunError::VmException` chain expected by the checkers. + fn hint_err(hint_error: HintError) -> std::result::Result<(), CairoRunError> { + Err(CairoRunError::VmException(VmException { + pc: Relocatable::from((0, 0)), + inst_location: None, + inner_exc: VirtualMachineError::Hint(Box::new((0, hint_error))), + error_attr_value: None, + traceback: None, + })) + } + + /// `assert_vm_result!(ok)` does not panic on `Ok`. + #[test] + fn assert_vm_result_ok_passes() { + assert_vm_result!(Ok::<(), i32>(()), ok); + } + + /// `assert_vm_result!(err pat)` does not panic when the error matches the pattern. + #[test] + fn assert_vm_result_err_passes() { + assert_vm_result!(Err::<(), i32>(42), err 42); + } + + /// `assert_vm_result!(err pat if guard)` does not panic when both pattern and guard match. + #[test] + fn assert_vm_result_err_with_guard_passes() { + assert_vm_result!(Err::<(), i32>(42), err x if *x == 42); + } + + /// `expect_ok` does not panic on `Ok(())`. + #[test] + fn expect_ok_passes() { + expect_ok(&Ok(())); + } + + /// `expect_hint_assert_not_zero` does not panic on `HintError::AssertNotZero`. + #[test] + fn expect_hint_assert_not_zero_passes() { + let res = hint_err(HintError::AssertNotZero(Box::default())); + expect_hint_assert_not_zero(&res); + } + + /// `expect_assert_not_equal_fail` does not panic on `HintError::AssertNotEqualFail`. + #[test] + fn expect_assert_not_equal_fail_passes() { + let res = hint_err(HintError::AssertNotEqualFail(Box::new(( + MaybeRelocatable::from(0), + MaybeRelocatable::from(0), + )))); + expect_assert_not_equal_fail(&res); + } + + /// `expect_diff_type_comparison` does not panic on `VirtualMachineError::DiffTypeComparison`. + #[test] + fn expect_diff_type_comparison_passes() { + let res = hint_err(HintError::Internal(VirtualMachineError::DiffTypeComparison( + Box::new((MaybeRelocatable::from(0), MaybeRelocatable::from((0, 0)))), + ))); + expect_diff_type_comparison(&res); + } + + /// `expect_diff_index_comp` does not panic on `VirtualMachineError::DiffIndexComp`. + #[test] + fn expect_diff_index_comp_passes() { + let res = hint_err(HintError::Internal(VirtualMachineError::DiffIndexComp( + Box::new((Relocatable::from((0, 0)), Relocatable::from((1, 0)))), + ))); + expect_diff_index_comp(&res); + } + + /// `expect_hint_value_outside_250_bit_range` does not panic on `HintError::ValueOutside250BitRange`. + #[test] + fn expect_hint_value_outside_250_bit_range_passes() { + let res = hint_err(HintError::ValueOutside250BitRange(Box::default())); + expect_hint_value_outside_250_bit_range(&res); + } + + /// `expect_non_le_felt252` does not panic on `HintError::NonLeFelt252`. + #[test] + fn expect_non_le_felt252_passes() { + let res = hint_err(HintError::NonLeFelt252(Box::default())); + expect_non_le_felt252(&res); + } + + /// `expect_assert_lt_felt252` does not panic on `HintError::AssertLtFelt252`. + #[test] + fn expect_assert_lt_felt252_passes() { + let res = hint_err(HintError::AssertLtFelt252(Box::default())); + expect_assert_lt_felt252(&res); + } + + /// `expect_hint_value_outside_valid_range` does not panic on `HintError::ValueOutsideValidRange`. + #[test] + fn expect_hint_value_outside_valid_range_passes() { + let res = hint_err(HintError::ValueOutsideValidRange(Box::default())); + expect_hint_value_outside_valid_range(&res); + } + + /// `expect_hint_out_of_valid_range` does not panic on `HintError::OutOfValidRange`. + #[test] + fn expect_hint_out_of_valid_range_passes() { + let res = hint_err(HintError::OutOfValidRange(Box::default())); + expect_hint_out_of_valid_range(&res); + } + + /// `expect_split_int_not_zero` does not panic on `HintError::SplitIntNotZero`. + #[test] + fn expect_split_int_not_zero_passes() { + let res = hint_err(HintError::SplitIntNotZero); + expect_split_int_not_zero(&res); + } + + /// `expect_split_int_limb_out_of_range` does not panic on `HintError::SplitIntLimbOutOfRange`. + #[test] + fn expect_split_int_limb_out_of_range_passes() { + let res = hint_err(HintError::SplitIntLimbOutOfRange(Box::default())); + expect_split_int_limb_out_of_range(&res); + } + + // --- unhappy path: wrong error variant should panic --- + + /// `assert_vm_result!(ok)` panics when given `Err`. + #[test] + #[should_panic(expected = "Expected Ok, got Err")] + fn assert_vm_result_ok_panics_on_err() { + assert_vm_result!(Err::<(), i32>(42), ok); + } + + /// `assert_vm_result!(err pat)` panics when given `Ok`. + #[test] + #[should_panic(expected = "Expected Err, got Ok")] + fn assert_vm_result_err_panics_on_ok() { + assert_vm_result!(Ok::<(), i32>(()), err 42); + } + + /// `assert_vm_result!(err pat)` panics when the error doesn't match the pattern. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn assert_vm_result_err_panics_on_wrong_variant() { + assert_vm_result!(Err::<(), i32>(1), err 42); + } + + /// `assert_vm_result!(err pat if guard)` panics when the guard fails. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn assert_vm_result_err_with_guard_panics_on_failed_guard() { + assert_vm_result!(Err::<(), i32>(42), err x if *x == 0); + } + + /// `expect_ok` panics when given an `Err`. + #[test] + #[should_panic(expected = "Expected Ok, got Err")] + fn expect_ok_panics_on_err() { + expect_ok(&hint_err(HintError::SplitIntNotZero)); + } + + /// Each `expect_*` checker panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_hint_assert_not_zero_panics_on_wrong_variant() { + expect_hint_assert_not_zero(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_assert_not_equal_fail` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_assert_not_equal_fail_panics_on_wrong_variant() { + expect_assert_not_equal_fail(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_diff_type_comparison` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_diff_type_comparison_panics_on_wrong_variant() { + expect_diff_type_comparison(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_diff_index_comp` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_diff_index_comp_panics_on_wrong_variant() { + expect_diff_index_comp(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_hint_value_outside_250_bit_range` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_hint_value_outside_250_bit_range_panics_on_wrong_variant() { + expect_hint_value_outside_250_bit_range(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_non_le_felt252` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_non_le_felt252_panics_on_wrong_variant() { + expect_non_le_felt252(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_assert_lt_felt252` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_assert_lt_felt252_panics_on_wrong_variant() { + expect_assert_lt_felt252(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_hint_value_outside_valid_range` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_hint_value_outside_valid_range_panics_on_wrong_variant() { + expect_hint_value_outside_valid_range(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_hint_out_of_valid_range` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_hint_out_of_valid_range_panics_on_wrong_variant() { + expect_hint_out_of_valid_range(&hint_err(HintError::SplitIntNotZero)); + } + + /// `expect_split_int_not_zero` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_split_int_not_zero_panics_on_wrong_variant() { + expect_split_int_not_zero(&hint_err(HintError::SplitIntLimbOutOfRange(Box::default()))); + } + + /// `expect_split_int_limb_out_of_range` panics when given a different error variant. + #[test] + #[should_panic(expected = "Unexpected error variant")] + fn expect_split_int_limb_out_of_range_panics_on_wrong_variant() { + expect_split_int_limb_out_of_range(&hint_err(HintError::SplitIntNotZero)); + } +} diff --git a/vm/src/test_helpers/test_utils.rs b/vm/src/test_helpers/test_utils.rs index 649cda3238..ed284e59a5 100644 --- a/vm/src/test_helpers/test_utils.rs +++ b/vm/src/test_helpers/test_utils.rs @@ -51,3 +51,90 @@ macro_rules! assert_mr_eq { assert_eq!($left, &right_mr, $($arg)+); }}; } + +#[cfg(test)] +mod tests { + use crate::types::relocatable::{MaybeRelocatable, Relocatable}; + + /// `load_cairo_program!` successfully loads a compiled Cairo program from the same directory. + /// + /// The source `dummy.cairo` used to produce `dummy.json` is: + /// ```cairo + /// func main() { + /// return (); + /// } + /// ``` + #[test] + fn load_cairo_program_loads_dummy() { + let program = load_cairo_program!("dummy.json"); + assert!(!program.shared_program_data.data.is_empty()); + } + + /// `load_cairo_program!` panics when the file does not exist. + #[test] + #[should_panic(expected = "Cairo program not found")] + fn load_cairo_program_panics_on_missing_file() { + load_cairo_program!("nonexistent.json"); + } + + /// `assert_mr_eq!` passes when an integer `MaybeRelocatable` equals the given felt value. + #[test] + fn assert_mr_eq_int_passes() { + let val = MaybeRelocatable::from(42); + assert_mr_eq!(&val, 42); + } + + /// `assert_mr_eq!` passes when a relocatable `MaybeRelocatable` equals the given pair. + #[test] + fn assert_mr_eq_relocatable_passes() { + let val = MaybeRelocatable::from(Relocatable::from((1, 5))); + assert_mr_eq!(&val, Relocatable::from((1, 5))); + } + + /// `assert_mr_eq!` passes with a custom message format. + #[test] + fn assert_mr_eq_with_message_passes() { + let val = MaybeRelocatable::from(7); + assert_mr_eq!(&val, 7, "value at index {} should be 7", 0); + } + + /// `assert_mr_eq!` panics when values differ. + #[test] + #[should_panic] + fn assert_mr_eq_panics_on_mismatch() { + let val = MaybeRelocatable::from(1); + assert_mr_eq!(&val, 2); + } + + /// `assert_mr_eq!` panics with a custom message when values differ. + #[test] + #[should_panic(expected = "wrong value")] + fn assert_mr_eq_with_message_panics_on_mismatch() { + let val = MaybeRelocatable::from(1); + assert_mr_eq!(&val, 2, "wrong value"); + } + + /// `assert_mr_eq!` panics when comparing a felt against a relocatable. + #[test] + #[should_panic] + fn assert_mr_eq_panics_felt_vs_relocatable() { + let val = MaybeRelocatable::from(1); + assert_mr_eq!(&val, Relocatable::from((0, 1))); + } + + /// `assert_mr_eq!` panics when relocatables have the same offset but different segments. + #[test] + #[should_panic] + fn assert_mr_eq_panics_relocatable_diff_segment() { + let val = MaybeRelocatable::from(Relocatable::from((0, 5))); + assert_mr_eq!(&val, Relocatable::from((1, 5))); + } + + /// `assert_mr_eq!` panics when relocatables have the same segment but different offsets. + #[test] + #[should_panic] + fn assert_mr_eq_panics_relocatable_diff_offset() { + let val = MaybeRelocatable::from(Relocatable::from((1, 0))); + assert_mr_eq!(&val, Relocatable::from((1, 1))); + } +} From b2f3db4af9952057fcf5907837131dec16f7f05a Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Sun, 29 Mar 2026 16:13:16 +0300 Subject: [PATCH 3/8] style: cargo fmt Co-Authored-By: Claude Sonnet 4.6 --- vm/src/test_helpers/error_utils.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vm/src/test_helpers/error_utils.rs b/vm/src/test_helpers/error_utils.rs index a4eabfbb18..115f696461 100644 --- a/vm/src/test_helpers/error_utils.rs +++ b/vm/src/test_helpers/error_utils.rs @@ -236,9 +236,12 @@ mod tests { /// `expect_diff_type_comparison` does not panic on `VirtualMachineError::DiffTypeComparison`. #[test] fn expect_diff_type_comparison_passes() { - let res = hint_err(HintError::Internal(VirtualMachineError::DiffTypeComparison( - Box::new((MaybeRelocatable::from(0), MaybeRelocatable::from((0, 0)))), - ))); + let res = hint_err(HintError::Internal( + VirtualMachineError::DiffTypeComparison(Box::new(( + MaybeRelocatable::from(0), + MaybeRelocatable::from((0, 0)), + ))), + )); expect_diff_type_comparison(&res); } From d8f4d0b83f52c0546678bf0bc29d5ddb8e8fd319 Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Sun, 29 Mar 2026 17:13:38 +0300 Subject: [PATCH 4/8] test: add conversion-failure and allow-large-err fixes to test_helpers - Add AlwaysFailConversion helper + 2 tests for assert_mr_eq! unwrap_or_else panic branch (no-message and message variants) - Allow clippy::result_large_err on hint_err test helper Co-Authored-By: Claude Sonnet 4.6 --- vm/src/test_helpers/error_utils.rs | 1 + vm/src/test_helpers/test_utils.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/vm/src/test_helpers/error_utils.rs b/vm/src/test_helpers/error_utils.rs index 115f696461..e30c10a63a 100644 --- a/vm/src/test_helpers/error_utils.rs +++ b/vm/src/test_helpers/error_utils.rs @@ -182,6 +182,7 @@ mod tests { use super::*; /// Wraps a `HintError` in the full `CairoRunError::VmException` chain expected by the checkers. + #[allow(clippy::result_large_err)] fn hint_err(hint_error: HintError) -> std::result::Result<(), CairoRunError> { Err(CairoRunError::VmException(VmException { pc: Relocatable::from((0, 0)), diff --git a/vm/src/test_helpers/test_utils.rs b/vm/src/test_helpers/test_utils.rs index ed284e59a5..df7d66106a 100644 --- a/vm/src/test_helpers/test_utils.rs +++ b/vm/src/test_helpers/test_utils.rs @@ -56,6 +56,17 @@ macro_rules! assert_mr_eq { mod tests { use crate::types::relocatable::{MaybeRelocatable, Relocatable}; + /// A type whose `TryInto` always fails, used to exercise + /// the `unwrap_or_else` panic branch in `assert_mr_eq!`. + struct AlwaysFailConversion; + + impl TryFrom for MaybeRelocatable { + type Error = &'static str; + fn try_from(_: AlwaysFailConversion) -> Result { + Err("intentional failure") + } + } + /// `load_cairo_program!` successfully loads a compiled Cairo program from the same directory. /// /// The source `dummy.cairo` used to produce `dummy.json` is: @@ -137,4 +148,20 @@ mod tests { let val = MaybeRelocatable::from(Relocatable::from((1, 0))); assert_mr_eq!(&val, Relocatable::from((1, 1))); } + + /// `assert_mr_eq!` (no-message variant) panics when `try_into` conversion fails. + #[test] + #[should_panic(expected = "conversion to MaybeRelocatable failed")] + fn assert_mr_eq_panics_on_conversion_failure() { + let val = MaybeRelocatable::from(42); + assert_mr_eq!(&val, AlwaysFailConversion); + } + + /// `assert_mr_eq!` (message variant) panics when `try_into` conversion fails. + #[test] + #[should_panic(expected = "conversion to MaybeRelocatable failed")] + fn assert_mr_eq_with_message_panics_on_conversion_failure() { + let val = MaybeRelocatable::from(42); + assert_mr_eq!(&val, AlwaysFailConversion, "should not reach assert_eq"); + } } From efedee90badf27c2f4286c015fa858f22de3abf5 Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Sun, 29 Mar 2026 17:18:18 +0300 Subject: [PATCH 5/8] chore: update CHANGELOG for PR #2378 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e8395764..797911c0fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Both branches support Stwo prover opcodes (Blake2s, QM31) since v2.0.0. #### Upcoming Changes * refactor: add `CairoFunctionRunner` type alias for `CairoRunner` under the `test_utils` feature flag [#2377](https://github.com/starkware-libs/cairo-vm/pull/2377) +* refactor: add `function_runner` feature flag and `CairoFunctionRunner` type alias for `CairoRunner` [#2377](https://github.com/starkware-libs/cairo-vm/pull/2377) +* feat: add `test_helpers` module (`error_utils`, `test_utils`) with `assert_mr_eq!`, `load_cairo_program!` macros and `expect_*` error checkers, behind `function_runner` feature flag [#2378](https://github.com/starkware-libs/cairo-vm/pull/2378) * Add Stwo cairo runner API [#2351](https://github.com/lambdaclass/cairo-vm/pull/2351) From 2d181d431fbb3576eace2977e0e085022be0c288 Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Sun, 29 Mar 2026 19:09:16 +0300 Subject: [PATCH 6/8] docs: mark load_cairo_program! example as ignore to suppress llvm-cov noise Co-Authored-By: Claude Sonnet 4.6 --- vm/src/test_helpers/test_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/src/test_helpers/test_utils.rs b/vm/src/test_helpers/test_utils.rs index df7d66106a..3e4c086a0e 100644 --- a/vm/src/test_helpers/test_utils.rs +++ b/vm/src/test_helpers/test_utils.rs @@ -4,7 +4,7 @@ /// via `file!()`, so the `.json` must live next to the `.cairo` and `.rs` files. /// /// # Example -/// ```rust +/// ```rust,ignore /// static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("main_math_test.json")); /// ``` /// From 238f84d811b0ef1dab8154049e6fc7431da4c3af Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Mon, 30 Mar 2026 11:42:49 +0300 Subject: [PATCH 7/8] fix: replace unwrap_or_else closures in macros to avoid llvm-cov empty function name error #[macro_export] macros containing closures (|x| ...) cause llvm-cov to emit a "function name is empty" error. Replaced unwrap_or_else(|e| panic!(...)) with match expressions to eliminate closures from macro expansions. Co-Authored-By: Claude Sonnet 4.6 --- vm/src/test_helpers/test_utils.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/vm/src/test_helpers/test_utils.rs b/vm/src/test_helpers/test_utils.rs index 3e4c086a0e..4c40f15a10 100644 --- a/vm/src/test_helpers/test_utils.rs +++ b/vm/src/test_helpers/test_utils.rs @@ -23,15 +23,18 @@ macro_rules! load_cairo_program { .join(file!()) .with_file_name($name); - let bytes = std::fs::read(&json_path).unwrap_or_else(|err| { - panic!( + let bytes = match std::fs::read(&json_path) { + Ok(b) => b, + Err(err) => panic!( "Cairo program not found at {json_path:?}: {err}\n\ Did you run `make cairo_test_suite_programs`?" - ) - }); + ), + }; - $crate::types::program::Program::from_bytes(&bytes, None) - .unwrap_or_else(|e| panic!("Failed to parse Cairo program at {json_path:?}: {e}")) + match $crate::types::program::Program::from_bytes(&bytes, None) { + Ok(p) => p, + Err(e) => panic!("Failed to parse Cairo program at {json_path:?}: {e}"), + } }}; } @@ -39,15 +42,17 @@ macro_rules! load_cairo_program { #[macro_export] macro_rules! assert_mr_eq { ($left:expr, $right:expr) => {{ - let right_mr = ($right) - .try_into() - .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + let right_mr = match ($right).try_into() { + Ok(v) => v, + Err(e) => panic!("conversion to MaybeRelocatable failed: {e:?}"), + }; assert_eq!($left, &right_mr); }}; ($left:expr, $right:expr, $($arg:tt)+) => {{ - let right_mr = ($right) - .try_into() - .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + let right_mr = match ($right).try_into() { + Ok(v) => v, + Err(e) => panic!("conversion to MaybeRelocatable failed: {e:?}"), + }; assert_eq!($left, &right_mr, $($arg)+); }}; } From 3e8d6ce832ec6123b0041c19225702183975f46f Mon Sep 17 00:00:00 2001 From: "Naor.d" Date: Mon, 6 Apr 2026 12:47:54 +0300 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20update=20function=5Frunner=20?= =?UTF-8?q?=E2=86=92=20test=5Futils=20cfg=20gates=20and=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to dropping the function_runner feature flag. Gate test_helpers module and function_runner module under test_utils, and update the doc comment in function_runner.rs accordingly. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- vm/src/lib.rs | 2 +- vm/src/vm/runners/function_runner.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 797911c0fe..28cf2b20cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Both branches support Stwo prover opcodes (Blake2s, QM31) since v2.0.0. * refactor: add `CairoFunctionRunner` type alias for `CairoRunner` under the `test_utils` feature flag [#2377](https://github.com/starkware-libs/cairo-vm/pull/2377) * refactor: add `function_runner` feature flag and `CairoFunctionRunner` type alias for `CairoRunner` [#2377](https://github.com/starkware-libs/cairo-vm/pull/2377) -* feat: add `test_helpers` module (`error_utils`, `test_utils`) with `assert_mr_eq!`, `load_cairo_program!` macros and `expect_*` error checkers, behind `function_runner` feature flag [#2378](https://github.com/starkware-libs/cairo-vm/pull/2378) +* feat: add `test_helpers` module (`error_utils`, `test_utils`) with `assert_mr_eq!`, `load_cairo_program!` macros and `expect_*` error checkers, behind `test_utils` feature flag [#2378](https://github.com/starkware-libs/cairo-vm/pull/2378) * Add Stwo cairo runner API [#2351](https://github.com/lambdaclass/cairo-vm/pull/2351) diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 2406f05bf1..dd485104bc 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -26,7 +26,7 @@ pub mod types; pub mod utils; pub mod vm; -#[cfg(feature = "function_runner")] +#[cfg(feature = "test_utils")] pub mod test_helpers; // TODO: use `Felt` directly diff --git a/vm/src/vm/runners/function_runner.rs b/vm/src/vm/runners/function_runner.rs index c584105710..e5b19eb0ff 100644 --- a/vm/src/vm/runners/function_runner.rs +++ b/vm/src/vm/runners/function_runner.rs @@ -1,7 +1,7 @@ //! Function runner extension methods for [`CairoRunner`]. //! //! Provides a simplified API for executing individual Cairo 0 functions by name or PC. -//! Enabled by the `function_runner` feature flag. +//! Enabled by the `test_utils` feature flag. use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor; use crate::hint_processor::hint_processor_definition::HintProcessor;