diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ade377346e..8ba9fd8c53 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,6 +18,7 @@ env: cairo_programs/**/*.json !cairo_programs/manually_compiled/* cairo_programs/cairo-1-programs/bitwise.sierra + vm/src/tests/cairo_test_suite/**/*.json TEST_COLLECT_COVERAGE: 1 PROPTEST_CASES: 100 @@ -46,6 +47,7 @@ jobs: - cairo_bench_programs - cairo_proof_programs - cairo_test_programs + - cairo_test_suite_programs - cairo_1_test_contracts - cairo_2_test_contracts name: Build Cairo programs @@ -66,7 +68,7 @@ jobs: id: cache-programs with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} # This is not pretty, but we need `make` to see the compiled programs are # actually newer than the sources, otherwise it will try to rebuild them @@ -120,37 +122,43 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_test_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_test_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch proof programs uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch bench programs uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_bench_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_bench_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} + fail-on-cache-miss: true + - name: Fetch cairo test suite programs + uses: actions/cache/restore@v3 + with: + path: ${{ env.CAIRO_PROGRAMS_PATH }} + key: cairo_test_suite_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch test contracts (Cairo 1) uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_1_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_1_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch test contracts (Cairo 2) uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_2_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_2_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Merge caches uses: actions/cache/save@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} lint: needs: merge-caches @@ -179,7 +187,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Run clippy @@ -229,7 +237,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true # NOTE: we do this separately because --workspace operates in weird ways @@ -274,7 +282,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Check all features (workspace) @@ -309,7 +317,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Install testing tools @@ -409,7 +417,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true # This is not pretty, but we need `make` to see the compiled programs are @@ -456,7 +464,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Generate traces @@ -675,7 +683,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Run script @@ -715,7 +723,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'vm/src/tests/cairo_test_suite/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch pie 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/CHANGELOG.md b/CHANGELOG.md index ff75eeea38..f0878ff44d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ Both branches support Stwo prover opcodes (Blake2s, QM31) since v2.0.0. --- #### Upcoming Changes +* feat: add `test_helpers` module (`error_utils`, `test_utils`) with `assert_mr_eq`, `load_cairo_program!` macro and `expect_*` error checkers, behind `test_utils` feature flag [#2381](https://github.com/starkware-libs/cairo-vm/pull/2381) + +* feat(makefile,ci): add `cairo_test_suite_programs` Makefile target and CI integration to compile Cairo test suite programs before running tests [#2380](https://github.com/starkware-libs/cairo-vm/pull/2380) * Add Stwo cairo runner API [#2351](https://github.com/starkware-libs/cairo-vm/pull/2351) * feat: refactor CairoRunner ctors to accept CairoLayout directly [#2363](https://github.com/starkware-libs/cairo-vm/pull/2363) diff --git a/Makefile b/Makefile index 1734325614..dbf76a3ab2 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ UNAME := $(shell uname) compare_trace_memory compare_trace compare_memory compare_pie compare_all_no_proof \ compare_trace_memory_proof compare_all_proof compare_trace_proof compare_memory_proof compare_air_public_input compare_air_private_input\ hyper-threading-benchmarks \ - cairo_bench_programs cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts \ + cairo_bench_programs cairo_proof_programs cairo_test_programs cairo_test_suite_programs cairo_1_test_contracts cairo_2_test_contracts \ cairo_trace cairo-vm_trace cairo_proof_trace cairo-vm_proof_trace python-deps python-deps-macos \ build-cairo-lang hint-accountant \ create-proof-programs-symlinks \ $(RELBIN) $(DBGBIN) @@ -124,6 +124,10 @@ NORETROCOMPAT_DIR:=cairo_programs/noretrocompat NORETROCOMPAT_FILES:=$(wildcard $(NORETROCOMPAT_DIR)/*.cairo) COMPILED_NORETROCOMPAT_TESTS:=$(patsubst $(NORETROCOMPAT_DIR)/%.cairo, $(NORETROCOMPAT_DIR)/%.json, $(NORETROCOMPAT_FILES)) +CAIRO_TEST_SUITE_ROOT:=vm/src/tests/cairo_test_suite +CAIRO_TEST_SUITE_FILES:=$(shell find $(CAIRO_TEST_SUITE_ROOT) -name "*.cairo") +COMPILED_CAIRO_TEST_SUITE:=$(patsubst %.cairo,%.json,$(CAIRO_TEST_SUITE_FILES)) + $(BENCH_DIR)/%.json: $(BENCH_DIR)/%.cairo cairo-compile --cairo_path="$(TEST_DIR):$(BENCH_DIR)" $< --output $@ --proof_mode @@ -145,6 +149,9 @@ $(BAD_TEST_DIR)/%.json: $(BAD_TEST_DIR)/%.cairo $(PRINT_TEST_DIR)/%.json: $(PRINT_TEST_DIR)/%.cairo cairo-compile $< --output $@ +$(CAIRO_TEST_SUITE_ROOT)/%.json: $(CAIRO_TEST_SUITE_ROOT)/%.cairo + cairo-compile $< --output $@ + # ====================== # Test Cairo 1 Contracts # ====================== @@ -268,6 +275,8 @@ run: check: cargo check +cairo_test_suite_programs: $(COMPILED_CAIRO_TEST_SUITE) + cairo_test_programs: $(COMPILED_TESTS) $(COMPILED_BAD_TESTS) $(COMPILED_NORETROCOMPAT_TESTS) $(COMPILED_PRINT_TESTS) $(COMPILED_MOD_BUILTIN_TESTS) $(COMPILED_SECP_CAIRO0_HINTS) $(COMPILED_KZG_DA_CAIRO0_HINTS) $(COMPILED_SEGMENT_ARENA_CAIRO0_HINTS) cairo_proof_programs: $(COMPILED_PROOF_TESTS) $(COMPILED_MOD_BUILTIN_PROOF_TESTS) $(COMPILED_STWO_EXCLUSIVE_TESTS) cairo_bench_programs: $(COMPILED_BENCHES) @@ -286,7 +295,7 @@ ifdef TEST_COLLECT_COVERAGE TEST_COMMAND:=cargo llvm-cov nextest --no-report endif -test: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts cairo_1_program +test: cairo_proof_programs cairo_test_programs cairo_test_suite_programs cairo_1_test_contracts cairo_2_test_contracts cairo_1_program $(TEST_COMMAND) --workspace --features "test_utils, cairo-1-hints" test-extensive_hints: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_1_program cairo_2_test_contracts $(TEST_COMMAND) --workspace --features "test_utils, cairo-1-hints, cairo-0-secp-hints, cairo-0-data-availability-hints, extensive_hints" diff --git a/vm/src/lib.rs b/vm/src/lib.rs index ab2bbc2e5f..dd485104bc 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 = "test_utils")] +pub mod test_helpers; + // TODO: use `Felt` directly pub use starknet_types_core::felt::Felt as Felt252; 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 new file mode 100644 index 0000000000..82299e453f --- /dev/null +++ b/vm/src/test_helpers/error_utils.rs @@ -0,0 +1,288 @@ +//! 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(&Result); + +/// Asserts that the result is `Ok`. +pub fn expect_ok(res: &Result<(), CairoRunError>) { + assert_vm_result!(res, ok); +} + +/// Asserts that the result is a `HintError` satisfying `predicate`. +fn expect_hint_error(res: &Result<(), CairoRunError>, predicate: impl Fn(&HintError) -> bool) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if predicate(&boxed.as_ref().1) + ); +} + +/// Asserts that the result is `HintError::AssertNotZero`. +pub fn expect_hint_assert_not_zero(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::AssertNotZero(_))); +} + +/// Asserts that the result is `HintError::AssertNotEqualFail`. +pub fn expect_assert_not_equal_fail(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::AssertNotEqualFail(_))); +} + +/// Asserts that the result is `VirtualMachineError::DiffTypeComparison` wrapped in a hint. +pub fn expect_diff_type_comparison(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| { + matches!( + e, + HintError::Internal(VirtualMachineError::DiffTypeComparison(_)) + ) + }); +} + +/// Asserts that the result is `VirtualMachineError::DiffIndexComp` wrapped in a hint. +pub fn expect_diff_index_comp(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| { + matches!( + e, + HintError::Internal(VirtualMachineError::DiffIndexComp(_)) + ) + }); +} + +/// Asserts that the result is `HintError::ValueOutside250BitRange`. +pub fn expect_hint_value_outside_250_bit_range(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::ValueOutside250BitRange(_))); +} + +/// Asserts that the result is `HintError::NonLeFelt252`. +pub fn expect_non_le_felt252(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::NonLeFelt252(_))); +} + +/// Asserts that the result is `HintError::AssertLtFelt252`. +pub fn expect_assert_lt_felt252(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::AssertLtFelt252(_))); +} + +/// Asserts that the result is `HintError::ValueOutsideValidRange`. +pub fn expect_hint_value_outside_valid_range(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::ValueOutsideValidRange(_))); +} + +/// Asserts that the result is `HintError::OutOfValidRange`. +pub fn expect_hint_out_of_valid_range(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::OutOfValidRange(_))); +} + +/// Asserts that the result is `HintError::SplitIntNotZero`. +pub fn expect_split_int_not_zero(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, HintError::SplitIntNotZero)); +} + +/// Asserts that the result is `HintError::SplitIntLimbOutOfRange`. +pub fn expect_split_int_limb_out_of_range(res: &Result<(), CairoRunError>) { + expect_hint_error(res, |e| matches!(e, 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 rstest::rstest; + + 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) -> Result<(), CairoRunError> { + Err(CairoRunError::VmException(VmException { + pc: Relocatable::default(), + 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(())); + } + + // --- happy path: each checker passes on its correct error variant --- + + #[rstest] + #[case::hint_assert_not_zero( + expect_hint_assert_not_zero, + hint_err(HintError::AssertNotZero(Box::default())) + )] + #[case::assert_not_equal_fail( + expect_assert_not_equal_fail, + hint_err(HintError::AssertNotEqualFail(Box::new(( + MaybeRelocatable::from(0), + MaybeRelocatable::from(0), + )))) + )] + #[case::diff_type_comparison( + expect_diff_type_comparison, + hint_err(HintError::Internal(VirtualMachineError::DiffTypeComparison(Box::new(( + MaybeRelocatable::from(0), + MaybeRelocatable::from((0, 0)), + ))))) + )] + #[case::diff_index_comp( + expect_diff_index_comp, + hint_err(HintError::Internal(VirtualMachineError::DiffIndexComp(Box::default()))) + )] + #[case::hint_value_outside_250_bit_range( + expect_hint_value_outside_250_bit_range, + hint_err(HintError::ValueOutside250BitRange(Box::default())) + )] + #[case::non_le_felt252( + expect_non_le_felt252, + hint_err(HintError::NonLeFelt252(Box::default())) + )] + #[case::assert_lt_felt252( + expect_assert_lt_felt252, + hint_err(HintError::AssertLtFelt252(Box::default())) + )] + #[case::hint_value_outside_valid_range( + expect_hint_value_outside_valid_range, + hint_err(HintError::ValueOutsideValidRange(Box::default())) + )] + #[case::hint_out_of_valid_range( + expect_hint_out_of_valid_range, + hint_err(HintError::OutOfValidRange(Box::default())) + )] + #[case::split_int_not_zero(expect_split_int_not_zero, hint_err(HintError::SplitIntNotZero))] + #[case::split_int_limb_out_of_range( + expect_split_int_limb_out_of_range, + hint_err(HintError::SplitIntLimbOutOfRange(Box::default())) + )] + fn checker_passes_on_correct_variant( + #[case] checker: VmCheck<()>, + #[case] res: Result<(), CairoRunError>, + ) { + checker(&res); + } + + // --- unhappy path: macro edge cases --- + + /// `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::Dummy)); + } + + // --- unhappy path: each checker panics on a wrong error variant --- + + #[rstest] + #[case::hint_assert_not_zero(expect_hint_assert_not_zero)] + #[case::assert_not_equal_fail(expect_assert_not_equal_fail)] + #[case::diff_type_comparison(expect_diff_type_comparison)] + #[case::diff_index_comp(expect_diff_index_comp)] + #[case::hint_value_outside_250_bit_range(expect_hint_value_outside_250_bit_range)] + #[case::non_le_felt252(expect_non_le_felt252)] + #[case::assert_lt_felt252(expect_assert_lt_felt252)] + #[case::hint_value_outside_valid_range(expect_hint_value_outside_valid_range)] + #[case::hint_out_of_valid_range(expect_hint_out_of_valid_range)] + #[case::split_int_not_zero(expect_split_int_not_zero)] + #[case::split_int_limb_out_of_range(expect_split_int_limb_out_of_range)] + #[should_panic(expected = "Unexpected error variant")] + fn hint_checker_panics_on_dummy_hint_error(#[case] checker: VmCheck<()>) { + checker(&hint_err(HintError::Dummy)); + } +} diff --git a/vm/src/test_helpers/mod.rs b/vm/src/test_helpers/mod.rs new file mode 100644 index 0000000000..41bbbe577a --- /dev/null +++ b/vm/src/test_helpers/mod.rs @@ -0,0 +1,4 @@ +//! Test helpers for Cairo VM — enabled by the `test_utils` feature. +#[cfg(feature = "test_utils")] +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..ee83c1cb1d --- /dev/null +++ b/vm/src/test_helpers/test_utils.rs @@ -0,0 +1,157 @@ +use crate::types::relocatable::MaybeRelocatable; + +/// 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,ignore +/// 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 = 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`?" + ), + }; + + match $crate::types::program::Program::from_bytes(&bytes, None) { + Ok(p) => p, + Err(e) => panic!("Failed to parse Cairo program at {json_path:?}: {e}"), + } + }}; +} + +/// Asserts that two values are equal after converting both to [`MaybeRelocatable`]. +/// +/// If the left conversion fails, the panic message says "left conversion … failed". +/// If the right conversion fails, it says "right conversion … failed". +#[track_caller] +pub fn assert_mr_eq(left: L, right: R) +where + L: TryInto, + L::Error: core::fmt::Debug, + R: TryInto, + R::Error: core::fmt::Debug, +{ + let left_mr = match left.try_into() { + Ok(v) => v, + Err(e) => panic!("left conversion to MaybeRelocatable failed: {e:?}"), + }; + let right_mr = match right.try_into() { + Ok(v) => v, + Err(e) => panic!("right conversion to MaybeRelocatable failed: {e:?}"), + }; + assert_eq!(left_mr, right_mr); +} + +#[cfg(test)] +mod tests { + use super::assert_mr_eq; + use crate::types::relocatable::{MaybeRelocatable, Relocatable}; + use rstest::rstest; + + /// A type whose `TryInto` always fails, used to exercise + /// the conversion-failure 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: + /// ```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: passing cases --- + + #[rstest] + #[case::int(MaybeRelocatable::from(42), MaybeRelocatable::from(42))] + #[case::relocatable( + MaybeRelocatable::from(Relocatable::from((1, 5))), + MaybeRelocatable::from(Relocatable::from((1, 5))) + )] + fn assert_mr_eq_passes(#[case] left: MaybeRelocatable, #[case] right: MaybeRelocatable) { + assert_mr_eq(left, right); + } + + // --- assert_mr_eq: mismatch panics --- + + #[rstest] + #[case::int_mismatch(MaybeRelocatable::from(1), MaybeRelocatable::from(2))] + #[case::felt_vs_relocatable( + MaybeRelocatable::from(1), + MaybeRelocatable::from(Relocatable::from((0, 1))) + )] + #[case::relocatable_diff_segment( + MaybeRelocatable::from(Relocatable::from((0, 5))), + MaybeRelocatable::from(Relocatable::from((1, 5))) + )] + #[case::relocatable_diff_offset( + MaybeRelocatable::from(Relocatable::from((1, 0))), + MaybeRelocatable::from(Relocatable::from((1, 1))) + )] + #[should_panic] + fn assert_mr_eq_panics_on_mismatch( + #[case] left: MaybeRelocatable, + #[case] right: MaybeRelocatable, + ) { + assert_mr_eq(left, right); + } + + // --- assert_mr_eq: conversion failure panics --- + + /// Panics with "right conversion" message when right `try_into` fails. + #[test] + #[should_panic(expected = "right conversion to MaybeRelocatable failed")] + fn assert_mr_eq_panics_on_right_conversion_failure() { + let val = MaybeRelocatable::from(42); + assert_mr_eq(&val, AlwaysFailConversion); + } + + /// Panics with "left conversion" message when left `try_into` fails. + #[test] + #[should_panic(expected = "left conversion to MaybeRelocatable failed")] + fn assert_mr_eq_panics_on_left_conversion_failure() { + let val = MaybeRelocatable::from(42); + assert_mr_eq(AlwaysFailConversion, &val); + } +} diff --git a/vm/src/vm/errors/hint_errors.rs b/vm/src/vm/errors/hint_errors.rs index 5dc4a60357..6d8eab1831 100644 --- a/vm/src/vm/errors/hint_errors.rs +++ b/vm/src/vm/errors/hint_errors.rs @@ -196,6 +196,9 @@ pub enum HintError { CircuitEvaluationFailed(Box), #[error("high limb magnitude should be smaller than 2 ** 127: {0}")] BlsSplitError(Box), + #[cfg(feature = "test_utils")] + #[error("dummy hint error for testing")] + Dummy, } #[cfg(test)] diff --git a/vm/src/vm/runners/function_runner.rs b/vm/src/vm/runners/function_runner.rs index 9b51864579..b0fcc8c461 100644 --- a/vm/src/vm/runners/function_runner.rs +++ b/vm/src/vm/runners/function_runner.rs @@ -20,7 +20,6 @@ use crate::vm::runners::cairo_runner::{CairoArg, CairoRunner, ORDERED_BUILTIN_LI use crate::vm::security::verify_secure_runner; /// Identifies a Cairo function entrypoint either by function name or by program counter. -#[allow(dead_code)] pub enum EntryPoint<'a> { Name(&'a str), Pc(usize), @@ -82,7 +81,7 @@ impl CairoRunner { /// Resolves the entrypoint, builds the call stack, runs until the function's end PC, /// and optionally verifies security constraints. #[allow(clippy::result_large_err)] - pub(crate) fn run_from_entrypoint( + pub fn run_from_entrypoint( &mut self, entrypoint: EntryPoint<'_>, args: &[CairoArg], @@ -113,7 +112,7 @@ impl CairoRunner { /// Resolves `__main__.` to its PC, following alias chains. #[allow(clippy::result_large_err)] - pub(crate) fn get_function_pc(&self, entrypoint: &str) -> Result { + pub fn get_function_pc(&self, entrypoint: &str) -> Result { let full_name = format!("__main__.{entrypoint}"); let identifier = self .program diff --git a/vm/src/vm/runners/mod.rs b/vm/src/vm/runners/mod.rs index 1bad5f2627..03f61b9fa1 100644 --- a/vm/src/vm/runners/mod.rs +++ b/vm/src/vm/runners/mod.rs @@ -2,4 +2,4 @@ pub mod builtin_runner; pub mod cairo_pie; pub mod cairo_runner; #[cfg(feature = "test_utils")] -pub(crate) mod function_runner; +pub mod function_runner;