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 f72857217c..f30a924b19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ 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) + +* feat: add math Cairo test suite under `vm/src/tests/cairo_test_suite` using the `test_utils` feature flag [#2379](https://github.com/starkware-libs/cairo-vm/pull/2379) * Add Stwo cairo runner API [#2351](https://github.com/lambdaclass/cairo-vm/pull/2351) 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/tests/cairo_test_suite/mod.rs b/vm/src/tests/cairo_test_suite/mod.rs new file mode 100644 index 0000000000..8007a32d0f --- /dev/null +++ b/vm/src/tests/cairo_test_suite/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "test_utils")] +mod test_math; diff --git a/vm/src/tests/cairo_test_suite/test_math/math_test.cairo b/vm/src/tests/cairo_test_suite/test_math/math_test.cairo new file mode 100644 index 0000000000..2584adc230 --- /dev/null +++ b/vm/src/tests/cairo_test_suite/test_math/math_test.cairo @@ -0,0 +1,31 @@ +%builtins range_check + +from starkware.cairo.common.math import ( + assert_not_zero, + assert_not_equal, + assert_nn, + assert_le, + assert_lt, + assert_nn_le, + assert_in_range, + assert_250_bit, + split_felt, + assert_le_felt, + assert_lt_felt, + abs_value, + sign, + unsigned_div_rem, + signed_div_rem, + safe_div, + safe_mult, + split_int, + sqrt, + horner_eval, + is_quad_residue, + assert_is_power_of_2, +) + + +func main{range_check_ptr}() { + return (); +} diff --git a/vm/src/tests/cairo_test_suite/test_math/math_test_utils.rs b/vm/src/tests/cairo_test_suite/test_math/math_test_utils.rs new file mode 100644 index 0000000000..df90cfbd49 --- /dev/null +++ b/vm/src/tests/cairo_test_suite/test_math/math_test_utils.rs @@ -0,0 +1,37 @@ +use std::sync::LazyLock; + +use crate::{math_utils::is_quad_residue, utils::CAIRO_PRIME}; +use num_bigint::BigUint; +use num_integer::Integer; + +/// RC_BOUND = 2^128 +pub static RC_BOUND: LazyLock = LazyLock::new(|| BigUint::from(2u64).pow(128)); + +/// MAX_DIV = CAIRO_PRIME // RC_BOUND +pub static MAX_DIV: LazyLock = LazyLock::new(|| CAIRO_PRIME.div_floor(&RC_BOUND)); + +/// Returns 1 if `a` is a quadratic residue modulo CAIRO_PRIME, 0 if not, and -1 on error. +pub fn is_quad_residue_mod_prime(a: &BigUint) -> i64 { + match is_quad_residue(a, &CAIRO_PRIME) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Returns 1 for a known quadratic residue: 4 = 2² mod CAIRO_PRIME. + #[test] + fn is_quad_residue_mod_prime_returns_1_for_residue() { + assert_eq!(is_quad_residue_mod_prime(&BigUint::from(4u32)), 1); + } + + /// Returns 0 for a known non-residue: 3 is not a square mod CAIRO_PRIME. + #[test] + fn is_quad_residue_mod_prime_returns_0_for_non_residue() { + assert_eq!(is_quad_residue_mod_prime(&BigUint::from(3u32)), 0); + } +} diff --git a/vm/src/tests/cairo_test_suite/test_math/mod.rs b/vm/src/tests/cairo_test_suite/test_math/mod.rs new file mode 100644 index 0000000000..a68b4b985f --- /dev/null +++ b/vm/src/tests/cairo_test_suite/test_math/mod.rs @@ -0,0 +1,2 @@ +mod math_test_utils; +mod test_math_cairo; diff --git a/vm/src/tests/cairo_test_suite/test_math/test_math_cairo.rs b/vm/src/tests/cairo_test_suite/test_math/test_math_cairo.rs new file mode 100644 index 0000000000..8789960301 --- /dev/null +++ b/vm/src/tests/cairo_test_suite/test_math/test_math_cairo.rs @@ -0,0 +1,928 @@ +//! Tests for `math.cairo`. + +use std::sync::LazyLock; + +use super::math_test_utils::{is_quad_residue_mod_prime, MAX_DIV, RC_BOUND}; +use crate::cairo_args; +use crate::load_cairo_program; +use crate::test_helpers::error_utils::{ + expect_assert_lt_felt252, expect_assert_not_equal_fail, expect_diff_index_comp, + expect_diff_type_comparison, expect_hint_assert_not_zero, expect_hint_out_of_valid_range, + expect_hint_value_outside_250_bit_range, expect_hint_value_outside_valid_range, + expect_non_le_felt252, expect_ok, expect_split_int_limb_out_of_range, + expect_split_int_not_zero, VmCheck, +}; +use crate::test_helpers::test_utils::assert_mr_eq; +use crate::types::builtin_name::BuiltinName; +use crate::types::program::Program; +use crate::types::relocatable::MaybeRelocatable; +use crate::utils::CAIRO_PRIME; +use crate::vm::runners::cairo_runner::CairoRunner; +use crate::Felt252; +use num_bigint::{BigInt, BigUint, RandBigInt}; +use num_traits::{One, Signed, Zero}; +use rand::thread_rng; +use rstest::{fixture, rstest}; + +// ===================== Shared constants (LazyLock) ===================== + +/// The compiled Cairo math program, loaded once and shared across all tests. +static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("math_test.json")); + +/// Interesting felt values used in several tests. +static INTERESTING_FELTS: LazyLock> = LazyLock::new(|| { + let p = &*CAIRO_PRIME; + vec![ + BigUint::zero(), + BigUint::one(), + BigUint::from(2u64).pow(128) - BigUint::one(), + BigUint::from(2u64).pow(128), + BigUint::from(2u64).pow(128) + BigUint::one(), + p / BigUint::from(3u64) - BigUint::one(), + p / BigUint::from(3u64), + p / BigUint::from(3u64) + BigUint::one(), + p / BigUint::from(2u64) - BigUint::one(), + p / BigUint::from(2u64), + p / BigUint::from(2u64) + BigUint::one(), + BigUint::from(2u64).pow(251) - BigUint::one(), + BigUint::from(2u64).pow(251), + BigUint::from(2u64).pow(251) + BigUint::one(), + p - BigUint::from(2u64), + p - BigUint::one(), + ] +}); + +// ===================== Helpers ===================== + +// ===================== Fixture ===================== + +/// Creates a fresh CairoRunner from the shared PROGRAM. +#[fixture] +fn runner() -> CairoRunner { + CairoRunner::new_for_testing(&PROGRAM).unwrap() +} + +// ===================== test_assert_not_zero ===================== + +#[rstest] +// Case: value=7 +// Expected: Success. +#[case(Some(BigUint::from(7u64)), expect_ok)] +// Case: value=random +// Expected: Success. +#[case::random(None, expect_ok)] +// Case: value=0 +// Expected: Error. +#[case(Some(BigUint::zero()), expect_hint_assert_not_zero)] +fn test_assert_not_zero(#[case] value: Option, #[case] check: VmCheck<()>) { + let value = match value { + Some(v) => v, + None => { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), &CAIRO_PRIME) + } + }; + + let mut runner = runner(); + let args = cairo_args!(value); + let res = runner.run_default_cairo0("assert_not_zero", &args); + check(&res); +} + +// ===================== test_assert_not_equal ===================== + +#[rstest] +// Not equal integers +// Case: a=3, b=7 +// Expected: Success. +#[case::not_equal_ints(MaybeRelocatable::from(3), MaybeRelocatable::from(7), expect_ok)] +// Not equal relocatables (same segment, different offset) +// Case: a=(2, 5), b=(2, 10) +// Expected: Success. +#[case::not_equal_relocs( + MaybeRelocatable::from((2, 5)), + MaybeRelocatable::from((2, 10)), + expect_ok +)] +// Equal integers +// Case: a=5, b=5 +// Expected: Error. +#[case::equal_ints( + MaybeRelocatable::from(5), + MaybeRelocatable::from(5), + expect_assert_not_equal_fail +)] +// Equal relocatables +// Case: a=(1, 5), b=(1, 5) +// Expected: Error. +#[case::equal_relocs( + MaybeRelocatable::from((1, 5)), + MaybeRelocatable::from((1, 5)), + expect_assert_not_equal_fail +)] +// Non-comparable: relocatable vs int +// Case: a=(1, 5), b=0 +// Expected: Error. +#[case::non_comparable_reloc_vs_int( + MaybeRelocatable::from((1, 5)), + MaybeRelocatable::from(0), + expect_diff_type_comparison +)] +// Non-comparable: different segments +// Case: a=(1, 5), b=(2, 3) +// Expected: Error. +#[case::non_comparable_diff_segments( + MaybeRelocatable::from((1, 5)), + MaybeRelocatable::from((2, 3)), + expect_diff_index_comp +)] + +fn test_assert_not_equal( + #[case] a: MaybeRelocatable, + #[case] b: MaybeRelocatable, + #[case] check: VmCheck<()>, +) { + let mut runner = runner(); + let args = cairo_args!(a, b); + let res = runner.run_default_cairo0("assert_not_equal", &args); + check(&res); +} + +// ===================== test_assert_250_bit ===================== +#[rstest] +// Valid cases (should pass) +// Case: value=0 +// Expected: Success. +#[case::zero(BigUint::zero(), expect_ok)] +// Case: value=1 +// Expected: Success. +#[case::one(BigUint::one(), expect_ok)] +// Case: value=(2^250)-1 +// Expected: Success. +#[case::max_valid(BigUint::from(2u64).pow(250) - BigUint::one(), expect_ok)] +// Invalid cases (should fail) +// Case: value=2^250 +// Expected: Error. +#[case::at_boundary(BigUint::from(2u64).pow(250), expect_hint_value_outside_250_bit_range)] +// Case: value=(2^250)+1 +// Expected: Error. +#[case::above_boundary(BigUint::from(2u64).pow(250) + BigUint::one(), expect_hint_value_outside_250_bit_range)] +// Case: value=2^251 +// Expected: Error. +#[case::way_above(BigUint::from(2u64).pow(251), expect_hint_value_outside_250_bit_range)] +// Case: value=PRIME-1 +// Expected: Error. +#[case::near_prime(&*CAIRO_PRIME - BigUint::one(), expect_hint_value_outside_250_bit_range)] +fn test_assert_250_bit( + mut runner: CairoRunner, + #[case] value: BigUint, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value); + let res = runner.run_default_cairo0("assert_250_bit", &args); + check(&res); + + // If successful, verify the return value + if res.is_ok() { + let ret = runner.vm.get_return_values(1).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(3).unwrap()); + } +} + +// ===================== test_split_felt ===================== + +#[rstest] +// Case: idx=0 +// Expected: Success. +#[case::idx_0(0)] +// Case: idx=1 +// Expected: Success. +#[case::idx_1(1)] +// Case: idx=2 +// Expected: Success. +#[case::idx_2(2)] +// Case: idx=3 +// Expected: Success. +#[case::idx_3(3)] +// Case: idx=4 +// Expected: Success. +#[case::idx_4(4)] +// Case: idx=5 +// Expected: Success. +#[case::idx_5(5)] +// Case: idx=6 +// Expected: Success. +#[case::idx_6(6)] +// Case: idx=7 +// Expected: Success. +#[case::idx_7(7)] +// Case: idx=8 +// Expected: Success. +#[case::idx_8(8)] +// Case: idx=9 +// Expected: Success. +#[case::idx_9(9)] +// Case: idx=10 +// Expected: Success. +#[case::idx_10(10)] +// Case: idx=11 +// Expected: Success. +#[case::idx_11(11)] +// Case: idx=12 +// Expected: Success. +#[case::idx_12(12)] +// Case: idx=13 +// Expected: Success. +#[case::idx_13(13)] +// Case: idx=14 +// Expected: Success. +#[case::idx_14(14)] +// Case: idx=15 +// Expected: Success. +#[case::idx_15(15)] +fn test_split_felt(mut runner: CairoRunner, #[case] idx: usize) { + let mask_128 = BigUint::from(2u64).pow(128) - BigUint::one(); + let value = &INTERESTING_FELTS[idx]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let expected_high: BigUint = value >> 128; + let expected_low = value & &mask_128; + + let args = cairo_args!(rc_base.clone(), value); + runner + .run_default_cairo0("split_felt", &args) + .unwrap_or_else(|e| panic!("split_felt failed for value {value}: {e}")); + + let ret = runner.vm.get_return_values(3).unwrap(); + // ret = [range_check_ptr, high, low] + assert_mr_eq(&ret[0], &rc_base.add_usize(3).unwrap()); + assert_mr_eq(&ret[1], &expected_high); + assert_mr_eq(&ret[2], &expected_low); +} + +// ===================== test_assert_le_felt ===================== + +#[rstest] +fn test_assert_le_felt( + mut runner: CairoRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value0, value1); + + if value0 <= value1 { + runner + .run_default_cairo0("assert_le_felt", &args) + .unwrap_or_else(|e| panic!("assert_le_felt failed for {value0} <= {value1}: {e}")); + let ret = runner.vm.get_return_values(1).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(4).unwrap()); + } else { + let result = runner.run_default_cairo0("assert_le_felt", &args); + expect_non_le_felt252(&result); + } +} + +// ===================== test_assert_lt_felt ===================== + +#[rstest] +fn test_assert_lt_felt( + mut runner: CairoRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value0, value1); + + if value0 < value1 { + runner + .run_default_cairo0("assert_lt_felt", &args) + .unwrap_or_else(|e| panic!("assert_lt_felt failed for {value0} < {value1}: {e}")); + let ret = runner.vm.get_return_values(1).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(4).unwrap()); + } else { + let result = runner.run_default_cairo0("assert_lt_felt", &args); + expect_assert_lt_felt252(&result); + } +} + +// ===================== test_abs_value ===================== + +#[rstest] +// Case: value_case=17 +// Expected: Success. +#[case(BigInt::from(17), expect_ok)] +// Case: value_case=-42 +// Expected: Success. +#[case(BigInt::from(-42), expect_ok)] +// Case: value_case=0 +// Expected: Success. +#[case(BigInt::from(0), expect_ok)] +// Case: value_case=RC_BOUND +// Expected: Error. +#[case(BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +// Case: value_case=-RC_BOUND +// Expected: Error. +#[case(-BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +fn test_abs_value(mut runner: CairoRunner, #[case] value_case: BigInt, #[case] check: VmCheck<()>) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + let rc_bound_biguint = runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + + let args = cairo_args!(rc_base.clone(), value_case.clone()); + let result = runner.run_default_cairo0("abs_value", &args); + check(&result); + let abs_value = value_case.magnitude(); + if abs_value < &rc_bound_biguint { + let ret = runner.vm.get_return_values(2).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(1).unwrap()); + assert_mr_eq(&ret[1], abs_value); + } +} + +// ===================== test_sign ===================== +#[rstest] +// Case: value_case=17 +// Expected: Success. +#[case(BigInt::from(17), expect_ok)] +// Case: value_case=-42 +// Expected: Success. +#[case(BigInt::from(-42), expect_ok)] +// Case: value_case=0 +// Expected: Success. +#[case(BigInt::from(0), expect_ok)] +// Case: value_case=RC_BOUND +// Expected: Error. +#[case(BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +// Case: value_case=-RC_BOUND +// Expected: Error. +#[case(-BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +fn test_sign(mut runner: CairoRunner, #[case] value_case: BigInt, #[case] check: VmCheck<()>) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + let rc_bound_biguint = runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + + let args = cairo_args!(rc_base.clone(), value_case.clone()); + let result = runner.run_default_cairo0("sign", &args); + check(&result); + let abs_value = value_case.magnitude(); + if abs_value < &rc_bound_biguint { + let ret = runner.vm.get_return_values(2).unwrap(); + // range_check_ptr == rc_base + (1 if value != 0 else 0) + let expected_rc_ptr = if value_case.is_zero() { + rc_base + } else { + rc_base.add_usize(1).unwrap() + }; + assert_mr_eq(&ret[0], &expected_rc_ptr); + + // res == (0 if value == 0 else 1 if value > 0 else PRIME - 1) + let expected_sign = if value_case.is_zero() { + BigUint::zero() + } else if value_case.is_positive() { + BigUint::one() + } else { + &*CAIRO_PRIME - BigUint::one() + }; + assert_mr_eq(&ret[1], &expected_sign); + } +} + +// ===================== test_unsigned_div_rem ===================== + +#[rstest] +// 1) q=1333, div=17, r=3 +// Case: q=1333, div=17, r=3 +// Expected: Success. +#[case::case_1_basic( + Some(BigUint::from(1333u64)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + expect_ok +)] +// 2) q=RC_BOUND-1, div=MAX_DIV, r=MAX_DIV-1 +// Case: q=RC_BOUND-1, div=MAX_DIV, r=MAX_DIV-1 +// Expected: Success. +#[case::case_2_max_values( + Some(&*RC_BOUND - BigUint::one()), + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + expect_ok +)] +// 3) q=random, div=MAX_DIV, r=0 +// Case: q=random, div=MAX_DIV, r=0 +// Expected: Success. +#[case::case_3_random_q( + None, + Some(MAX_DIV.clone()), + Some(BigUint::zero()), + expect_ok +)] +// 4) q=random, div=MAX_DIV, r=MAX_DIV-1 +// Case: q=random, div=MAX_DIV, r=MAX_DIV-1 +// Expected: Success. +#[case::case_4_random_q( + None, + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + expect_ok +)] +// 5) q=random, div=MAX_DIV, r=random +// Case: q=random, div=MAX_DIV, r=random +// Expected: Success. +#[case::case_5_random_q_and_r( + None, + Some(MAX_DIV.clone()), + None, + expect_ok +)] +// 6) q=random, div=random, r=random +// Case: q=random, div=random, r=random +// Expected: Success. +#[case::case_6_all_random(None, None, None, expect_ok)] +// 7) q=1, div=MAX_DIV+1, r=random -> expected error. +// Case: q=1, div=MAX_DIV+1, r=random +// Expected: Error. +#[case::case_7_invalid_div( + Some(BigUint::one()), + Some(&*MAX_DIV + BigUint::one()), + None, + expect_hint_out_of_valid_range +)] +fn test_unsigned_div_rem( + mut runner: CairoRunner, + #[case] q: Option, + #[case] div: Option, + #[case] r: Option, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + // Verify rc_bound matches expected RC_BOUND (2^128) + let rc_bound = runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + assert_eq!(rc_bound, *RC_BOUND, "Unexpected rc_bound"); + + let mut rng = thread_rng(); + // Python uses div in [0, MAX_DIV], but remainder generation requires div > 0. + let div = match div { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::one(), &(&*MAX_DIV + BigUint::one())), + }; + let r = match r { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &div), + }; + let q = match q { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &RC_BOUND), + }; + + let value = &q * &div + &r; + + // Assert value < PRIME (as in Python test) + assert!( + value < *CAIRO_PRIME, + "Generated value is too large. q={q}, div={div}, r={r}" + ); + + let args = cairo_args!(rc_base.clone(), value, div); + let result = runner.run_default_cairo0("unsigned_div_rem", &args); + check(&result); + + // If successful, verify the results match expected values + if result.is_ok() { + let ret = runner.vm.get_return_values(3).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(3).unwrap()); + assert_mr_eq(&ret[1], &q); + assert_mr_eq(&ret[2], &r); + } +} + +// ===================== test_signed_div_rem ===================== +#[rstest] +// Case: q=1333, div=17, r=3, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Success. +#[case::basic( + Some(BigInt::from(1333)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + None, + expect_ok +)] +// Case: q=-1333, div=17, r=3, bound=random in chosen range [-q,RC_BOUND/2]) +// Expected: Success. +#[case::negative_basic( + Some(BigInt::from(-1333)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + None, + expect_ok +)] +// Case: q=RC_BOUND/2-1, div=MAX_DIV, r=MAX_DIV-1, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Success. +#[case::max_pos( + Some(BigInt::from(&*RC_BOUND / BigUint::from(2u64) - BigUint::one())), + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + None, + expect_ok +)] +// Case: q=-(RC_BOUND/2)+1, div=MAX_DIV, r=0, bound=random in chosen range [-q,RC_BOUND/2]) +// Expected: Success. +#[case::max_neg( + Some(-(BigInt::from(&*RC_BOUND / BigUint::from(2u64))) + BigInt::one()), + Some(MAX_DIV.clone()), + Some(BigUint::zero()), + None, + expect_ok +)] +// Case: q=random, div=MAX_DIV, r=0, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_r_zero(None, Some(MAX_DIV.clone()), Some(BigUint::zero()), None, expect_ok)] +// Case: q=random, div=MAX_DIV, r=MAX_DIV-1, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_r_max(None, Some(MAX_DIV.clone()), Some(&*MAX_DIV - BigUint::one()), None, expect_ok)] +// Case: q=random, div=MAX_DIV, r=random, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_random_r(None, Some(MAX_DIV.clone()), None, None, expect_ok)] +// Case: q=RC_BOUND/2-1, div=random, r=random, bound=RC_BOUND/2 +// Expected: Success. +#[case::bound_eq_half_pos_q( + Some(BigInt::from(&*RC_BOUND / BigUint::from(2u64) - BigUint::one())), + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64)), + expect_ok +)] +// Case: q=-RC_BOUND/2, div=random, r=random, bound=RC_BOUND/2 +// Expected: Success. +#[case::bound_eq_half_neg_q( + Some(-BigInt::from(&*RC_BOUND / BigUint::from(2u64))), + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64)), + expect_ok +)] +// Case: q=1, div=MAX_DIV+1, r=random, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Error. +#[case::invalid_div( + Some(BigInt::one()), + Some(&*MAX_DIV + BigUint::one()), + None, + None, + expect_hint_out_of_valid_range +)] +// Case: q=random, div=random, r=random, bound=RC_BOUND/2+1 +// Expected: Error. +#[case::invalid_bound( + None, + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64) + BigUint::one()), + expect_hint_out_of_valid_range +)] +fn test_signed_div_rem( + mut runner: CairoRunner, + #[case] q: Option, + #[case] div: Option, + #[case] r: Option, + #[case] bound: Option, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let half_rc_bound = &*RC_BOUND / BigUint::from(2u64); + let mut rng = thread_rng(); + + let div = match div { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::one(), &(&*MAX_DIV + BigUint::one())), + }; + let r = match r { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &div), + }; + let q = match q { + Some(v) => v, + None => { + let min = -BigInt::from(half_rc_bound.clone()); + let max = BigInt::from(half_rc_bound.clone()); + rng.gen_bigint_range(&min, &max) + } + }; + let bound = match bound { + Some(v) => v, + None => { + let lower = if q >= BigInt::zero() { + q.clone() + BigInt::one() + } else { + -q.clone() + }; + let upper = BigInt::from(half_rc_bound.clone() + BigUint::one()); + rng.gen_bigint_range(&lower, &upper) + .to_biguint() + .expect("bound should be non-negative") + } + }; + + let value = q.clone() * BigInt::from(div.clone()) + BigInt::from(r.clone()); + let half_prime = BigInt::from((&*CAIRO_PRIME) >> 1); + let neg_half_prime = -half_prime.clone(); + assert!( + value >= neg_half_prime && value < half_prime, + "Generated value is too large." + ); + + let args = cairo_args!(rc_base.clone(), value, div, bound); + let result = runner.run_default_cairo0("signed_div_rem", &args); + check(&result); + + if result.is_ok() { + let ret = runner.vm.get_return_values(3).unwrap(); + let rc_ptr = &ret[0]; + let result_q = &ret[1]; + let result_r = &ret[2]; + + assert_mr_eq(rc_ptr, &rc_base.add_usize(4).unwrap()); + // Expected_q = q % PRIME (field element conversion). + let expected_q = Felt252::from(&q); + assert_mr_eq(result_q, &expected_q); + assert_mr_eq(result_r, &r); + } +} + +// ===================== test_split_int ===================== +#[rstest] +// Case: value=0x1234FCDA, n=10, base=16, bound=16, expected_output=vec![0xA, 0xD, +// 0xC, 0xF, 0x4, 0x3, 0x2, 0x1, 0, 0] Expected: Success. +#[case::hex_digits( + 0x1234FCDA, + 10, + 16, + 16, + Some(vec![0xA, 0xD, 0xC, 0xF, 0x4, 0x3, 0x2, 0x1, 0, 0]), + expect_ok +)] +// Case: value=0x1234FCDA, n=10, base=256, bound=256, expected_output=vec![0xDA, +// 0xFC, 0x34, 0x12, 0, 0, 0, 0, 0, 0] Expected: Success. +#[case::byte_pairs( + 0x1234FCDA, + 10, + 256, + 256, + Some(vec![0xDA, 0xFC, 0x34, 0x12, 0, 0, 0, 0, 0, 0]), + expect_ok +)] +// Case: value=0x1234FCDA, n=10, base=16, bound=15, expected_output=random +// Expected: Error. +#[case::out_of_bound_limb(0x1234FCDA, 10, 16, 15, None, expect_split_int_limb_out_of_range)] +// Case: value=0xAAA, n=3, base=16, bound=11, expected_output=vec![0xA, 0xA, 0xA] +// Expected: Success. +#[case::exact_fit( + 0xAAA, + 3, + 16, + 11, + Some(vec![0xA, 0xA, 0xA]), + expect_ok +)] +// Case: value=0xAAA, n=3, base=16, bound=10, expected_output=random +// Expected: Error. +#[case::bound_too_small(0xAAA, 3, 16, 10, None, expect_split_int_limb_out_of_range)] +// Case: value=0xAAA, n=2, base=16, bound=16, expected_output=random +// Expected: Error. +#[case::value_out_of_range(0xAAA, 2, 16, 16, None, expect_split_int_not_zero)] +fn test_split_int( + mut runner: CairoRunner, + #[case] value: i64, + #[case] n: i64, + #[case] base: i64, + #[case] bound: i64, + #[case] expected_output: Option>, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let output = runner.vm.add_memory_segment(); + let output_mr = MaybeRelocatable::from(output); + + let args = cairo_args!(rc_base.clone(), value, n, base, bound, output_mr); + let result = runner.run_default_cairo0("split_int", &args); + check(&result); + + if result.is_ok() { + let expected_output = + expected_output.expect("expected_output must be set for success case"); + let ret = runner.vm.get_return_values(1).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(2 * n as usize).unwrap()); + + let range = runner.vm.get_range(output, n as usize); + assert_eq!( + range.len(), + expected_output.len(), + "split_int output length mismatch" + ); + for (i, (actual, exp)) in range.iter().zip(expected_output.iter()).enumerate() { + let actual_val = actual + .as_ref() + .unwrap_or_else(|| panic!("Missing output at index {i}")); + assert_mr_eq(actual_val.as_ref(), *exp); + } + } +} +// ===================== test_sqrt ===================== + +#[rstest] +// Case: value=0 +// Expected: Success. +#[case::zero(Some(BigUint::zero()), expect_ok)] +// Case: value=1 +// Expected: Success. +#[case::one(Some(BigUint::one()), expect_ok)] +// Case: value=2 +// Expected: Success. +#[case::two(Some(BigUint::from(2u64)), expect_ok)] +// Case: value=3 +// Expected: Success. +#[case::three(Some(BigUint::from(3u64)), expect_ok)] +// Case: value=4 +// Expected: Success. +#[case::four(Some(BigUint::from(4u64)), expect_ok)] +// Case: value=5 +// Expected: Success. +#[case::five(Some(BigUint::from(5u64)), expect_ok)] +// Case: value=6 +// Expected: Success. +#[case::six(Some(BigUint::from(6u64)), expect_ok)] +// Case: value=7 +// Expected: Success. +#[case::seven(Some(BigUint::from(7u64)), expect_ok)] +// Case: value=8 +// Expected: Success. +#[case::eight(Some(BigUint::from(8u64)), expect_ok)] +// Case: value=9 +// Expected: Success. +#[case::nine(Some(BigUint::from(9u64)), expect_ok)] +// Case: value=(2^250)-1 +// Expected: Success. +#[case::max_valid(Some(BigUint::from(2u64).pow(250) - BigUint::one()), expect_ok)] +// Case: value=random +// Expected: Success. +#[case::random(None, expect_ok)] +// Case: value=2^250 +// Expected: Error. +#[case::out_of_range_2_pow_250(Some(BigUint::from(2u64).pow(250)), expect_hint_value_outside_250_bit_range)] +// Case: value=PRIME-1 +// Expected: Error. +#[case::out_of_range_prime_minus_one( + Some(&*CAIRO_PRIME - BigUint::one()), + expect_hint_value_outside_250_bit_range +)] +fn test_sqrt(mut runner: CairoRunner, #[case] value: Option, #[case] check: VmCheck<()>) { + let value = value.unwrap_or_else(|| { + let mut rng = thread_rng(); + let upper = BigUint::from(2u64).pow(250); + rng.gen_biguint_range(&BigUint::zero(), &upper) + }); + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value.clone()); + let result = runner.run_default_cairo0("sqrt", &args); + check(&result); + + if result.is_ok() { + let ret = runner.vm.get_return_values(2).unwrap(); + assert_mr_eq(&ret[0], &rc_base.add_usize(4).unwrap()); + + let expected_root = value.sqrt(); + assert_mr_eq(&ret[1], &expected_root); + } +} + +// ===================== test_horner_eval ===================== + +#[rstest] +// Case: n=0 +// Expected: Success. +#[case::zero_coefficients(0)] +// Case: n=16 +// Expected: Success. +#[case::sixteen_coefficients(16)] +fn test_horner_eval(mut runner: CairoRunner, #[case] n: usize) { + let mut rng = thread_rng(); + let prime = &*CAIRO_PRIME; + + // Generate random coefficients in [0, PRIME) + let coefficients: Vec = (0..n) + .map(|_| rng.gen_biguint_range(&BigUint::zero(), prime)) + .collect(); + let coeff_mr: Vec = coefficients.iter().map(MaybeRelocatable::from).collect(); + + // Generate random point in [0, PRIME) + let point = rng.gen_biguint_range(&BigUint::zero(), prime); + + // horner_eval takes (n, coefficients_ptr, point) - coefficients is an array + let args = cairo_args!(n, coeff_mr, point.clone()); + runner.run_default_cairo0("horner_eval", &args).unwrap(); + + let ret = runner.vm.get_return_values(1).unwrap(); + + // Compute expected result: sum(coef * point^i for i, coef in enumerate(coefficients)) % PRIME + let expected: BigUint = coefficients + .iter() + .enumerate() + .map(|(i, coef)| coef * point.modpow(&BigUint::from(i), prime)) + .fold(BigUint::zero(), |acc, x| (acc + x) % prime); + + assert_mr_eq(&ret[0], &expected); +} + +// ===================== test_is_quad_residue ===================== + +#[rstest] +// Case: x=0 +// Expected: Success. +#[case::zero(Some(BigUint::zero()))] +// Case: x=random +// Expected: Success. +#[case::random(None)] +fn test_is_quad_residue(mut runner: CairoRunner, #[case] x: Option) { + let prime = &*CAIRO_PRIME; + + let x = x.unwrap_or_else(|| { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), prime) + }); + + // Test is_quad_residue(x) + let args = cairo_args!(x.clone()); + runner.run_default_cairo0("is_quad_residue", &args).unwrap(); + let ret = runner.vm.get_return_values(1).unwrap(); + + let expected = is_quad_residue_mod_prime(&x); + assert_mr_eq(&ret[0], expected); + + // Test is_quad_residue(3 * x) + // 3 is not a quadratic residue modulo PRIME + let mut runner2 = CairoRunner::new_for_testing(&PROGRAM).unwrap(); + let three_x = (BigUint::from(3u64) * &x) % prime; + let args2 = cairo_args!(three_x); + runner2 + .run_default_cairo0("is_quad_residue", &args2) + .unwrap(); + let ret2 = runner2.vm.get_return_values(1).unwrap(); + + let expected2: i64 = if x.is_zero() { + 1 // 3 * 0 = 0, which is QR + } else if is_quad_residue_mod_prime(&x) == 1 { + 0 // x is QR, 3 is not QR, so 3*x is not QR + } else { + 1 // x is not QR, 3 is not QR, so 3*x is QR (product of two non-QR is QR) + }; + assert_mr_eq(&ret2[0], expected2); +} diff --git a/vm/src/tests/mod.rs b/vm/src/tests/mod.rs index 342b912d34..d222c777d7 100644 --- a/vm/src/tests/mod.rs +++ b/vm/src/tests/mod.rs @@ -36,6 +36,9 @@ mod cairo_pie_test; #[cfg(feature = "test_utils")] mod skip_instruction_test; +#[cfg(feature = "test_utils")] +mod cairo_test_suite; + //For simple programs that should just succeed and have no special needs. //Checks memory holes == 0 fn run_program_simple(data: &[u8]) { 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 96f3b2ee67..9ac098091a 100644 --- a/vm/src/vm/runners/function_runner.rs +++ b/vm/src/vm/runners/function_runner.rs @@ -19,7 +19,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), @@ -75,7 +74,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], @@ -106,7 +105,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;