Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(wip) feat: scarb pytest #1490

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cairo/kakarot-ssj/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ starknet = "2.8.2"

[workspace.tool.fmt]
sort-module-level-items = true

[features]
default = []
pytest = []
4 changes: 4 additions & 0 deletions cairo/kakarot-ssj/crates/evm/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ fmt.workspace = true
[scripts]
test = "snforge test --max-n-steps 4294967295"
test-profiling = "snforge test --max-n-steps 4294967295 --build-profile"

[features]
default = []
pytest = []
30 changes: 15 additions & 15 deletions cairo/kakarot-ssj/crates/evm/src/errors.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,25 @@ pub enum EVMError {
pub impl EVMErrorImpl of EVMErrorTrait {
fn to_string(self: EVMError) -> felt252 {
match self {
EVMError::StackOverflow => 'stack overflow',
EVMError::StackUnderflow => 'stack underflow',
EVMError::StackOverflow => 'StackOverflow',
EVMError::StackUnderflow => 'StackUnderflow',
EVMError::TypeConversionError(error_message) => error_message,
EVMError::NumericOperations(error_message) => error_message,
EVMError::InsufficientBalance => 'insufficient balance',
EVMError::ReturnDataOutOfBounds => 'return data out of bounds',
EVMError::InvalidJump => 'invalid jump destination',
EVMError::InvalidCode => 'invalid code',
EVMError::NotImplemented => 'not implemented',
EVMError::InsufficientBalance => 'InsufficientBalance',
EVMError::ReturnDataOutOfBounds => 'ReturnDataOutOfBounds',
EVMError::InvalidJump => 'InvalidJump',
EVMError::InvalidCode => 'InvalidCode',
EVMError::NotImplemented => 'NotImplemented',
EVMError::InvalidParameter(error_message) => error_message,
// TODO: refactor with dynamic strings once supported
EVMError::InvalidOpcode => 'invalid opcode'.into(),
EVMError::WriteInStaticContext => 'write protection',
EVMError::Collision => 'create collision'.into(),
EVMError::OutOfGas => 'out of gas'.into(),
EVMError::Assertion => 'assertion failed'.into(),
EVMError::DepthLimit => 'max call depth exceeded'.into(),
EVMError::MemoryLimitOOG => 'memory limit out of gas'.into(),
EVMError::NonceOverflow => 'nonce overflow'.into(),
EVMError::InvalidOpcode => 'InvalidOpcode',
EVMError::WriteInStaticContext => 'WriteInStaticContext',
EVMError::Collision => 'Collision',
EVMError::OutOfGas => 'OutOfGas',
EVMError::Assertion => 'Assertion',
EVMError::DepthLimit => 'DepthLimit',
EVMError::MemoryLimitOOG => 'MemoryLimitOOG',
EVMError::NonceOverflow => 'NonceOverflow',
}
}

Expand Down
131 changes: 130 additions & 1 deletion cairo/kakarot-ssj/crates/evm/src/stack.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,17 @@ impl StackImpl of StackTrait {
fn pop_n(ref self: Stack, mut n: usize) -> Result<Array<u256>, EVMError> {
ensure(!(n > self.len()), EVMError::StackUnderflow)?;
let mut popped_items = ArrayTrait::<u256>::new();
let mut err = Result::Ok(array![]);
for _ in 0..n {
popped_items.append(self.pop().unwrap());
let popped_item = self.pop();
match popped_item {
Result::Ok(item) => popped_items.append(item),
Result::Err(pop_error) => { err = Result::Err(pop_error); break;},
};
};
if err.is_err() {
return err;
}
Result::Ok(popped_items)
}

Expand Down Expand Up @@ -634,3 +642,124 @@ mod tests {
}
}
}

#[cfg(feature: 'pytest')]
mod pytests {
//! Pytests are tests that are run with the scarb-pytest framework.
//! This framework allows for testing based on various inputs provided by a third-party test
//! runner such as pytest or cargo test.
use core::fmt::{Formatter};
use crate::errors::{EVMErrorTrait};
use utils::pytests::json::{JsonMut, Json};
use utils::pytests::from_array::FromArray;
use crate::stack::{Stack, StackTrait};

impl StackJSON of JsonMut<Stack> {
fn to_json(ref self: Stack) -> ByteArray {
let mut json: ByteArray = "";
let mut formatter = Formatter { buffer: json };
write!(formatter, "[").unwrap();
for i in 0
..self
.len() {
let item = self.items.get(i.into()).deref();
write!(formatter, "{}", item).unwrap();
if i != self.len() - 1 {
write!(formatter, ", ").unwrap();
}
};
write!(formatter, "]").unwrap();
formatter.buffer
}
}

impl StackFromArray of FromArray<u256> {
type Output = Stack;
fn from_array(array: Span<u256>) -> Self::Output {
let mut stack = StackTrait::new();
for item in array {
stack.push(*item).expect('Stack FromArray failed');
};
stack
}
}

fn test__stack_push(values: Span<u256>) -> ByteArray {
let mut stack = StackTrait::new();
let mut err = Result::Ok(());
for value in values {
match stack.push(*value) {
Result::Ok(()) => (),
Result::Err(evm_error) => {
err = Result::Err(evm_error);
break;
},
};
};
if err.is_err() {
core::panic_with_felt252(err.unwrap_err().to_string());
};
stack.to_json()
}

fn test__stack_pop(stack: Span<u256>) -> ByteArray {
let mut stack = StackFromArray::from_array(stack);
let mut err = Result::Ok(());
let value = match stack.pop() {
Result::Ok(value) => value,
Result::Err(evm_error) => { err = Result::Err(evm_error); 0},
};
if err.is_err() {
core::panic_with_felt252(err.unwrap_err().to_string());
};
let mut output: (Stack, u256) = (stack, value);
output.to_json()
}

fn test__stack_pop_n(stack: Span<u256>, n: usize) -> ByteArray {
let mut stack = StackFromArray::from_array(stack);
let mut err = Result::Ok(());
let values = match stack.pop_n(n) {
Result::Ok(values) => values,
Result::Err(evm_error) => { err = Result::Err(evm_error); array![]},
};
if err.is_err() {
core::panic_with_felt252(err.unwrap_err().to_string());
};
let mut output: (Stack, Span<u256>) = (stack, values.span());
output.to_json()
}

fn test__stack_peek(stack: Span<u256>, index: usize) -> ByteArray {
let mut stack = StackFromArray::from_array(stack);
let mut err = Result::Ok(());
let value = match stack.peek_at(index) {
Result::Ok(value) => value,
Result::Err(evm_error) => { err = Result::Err(evm_error); 0},
};
if err.is_err() {
core::panic_with_felt252(err.unwrap_err().to_string());
};

let mut output: (Stack, u256) = (stack, value);
output.to_json()
}

fn test__stack_swap(stack: Span<u256>, index: usize) -> ByteArray {
let mut stack = StackFromArray::from_array(stack);
let mut err = Result::Ok(());
match stack.swap_i(index) {
Result::Ok(()) => (),
Result::Err(evm_error) => { err = Result::Err(evm_error); },
};
if err.is_err() {
core::panic_with_felt252(err.unwrap_err().to_string());
};
stack.to_json()
}

fn test__stack_new() -> ByteArray {
let mut stack: Stack = Default::default();
stack.to_json()
}
}
4 changes: 4 additions & 0 deletions cairo/kakarot-ssj/crates/utils/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ assert_macros = "2.8.2"
[scripts]
test = "snforge test --max-n-steps 4294967295"
test-profiling = "snforge test --max-n-steps 4294967295 --build-profile"

[features]
default = []
pytest = []
6 changes: 6 additions & 0 deletions cairo/kakarot-ssj/crates/utils/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ pub mod set;
pub mod test_data;
pub mod traits;
pub mod utils;

// #[cfg(feature: 'pytest')]
pub mod pytests {
pub mod json;
pub mod from_array;
}
4 changes: 4 additions & 0 deletions cairo/kakarot-ssj/crates/utils/src/pytests/from_array.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub trait FromArray<T> {
type Output;
fn from_array(array: Span<T>) -> Self::Output;
}
84 changes: 84 additions & 0 deletions cairo/kakarot-ssj/crates/utils/src/pytests/json.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use core::fmt::Formatter;

pub trait JsonMut<T> {
fn to_json(ref self: T) -> ByteArray;
}

pub trait Json<T> {
fn to_json(self: @T) -> ByteArray;
}

impl TupleTwoJson<T1, T2, +Destruct<T1>, +Json<T1>, +Destruct<T2>, +Json<T2>> of Json<(T1, T2)> {
fn to_json(self: @(T1, T2)) -> ByteArray {
let (t1, t2) = self;
format!("[{}, {}]", t1.to_json(), t2.to_json())
}
}

impl TupleThreeJson<T1, T2, T3, +Destruct<T1>, +Json<T1>, +Destruct<T2>, +Json<T2>, +Destruct<T3>, +Json<T3>> of Json<(T1, T2, T3)> {
fn to_json(self: @(T1, T2, T3)) -> ByteArray {
let (t1, t2, t3) = self;
format!("[{}, {}, {}]", t1.to_json(), t2.to_json(), t3.to_json())
}
}

impl TupleTwoJsonMut<T1, T2, +Destruct<T1>, +JsonMut<T1>, +Destruct<T2>, +Json<T2>> of JsonMut<(T1, T2)> {
fn to_json(ref self: (T1, T2)) -> ByteArray {
let (mut t1, mut t2) = self;
let res = format!("[{}, {}]", t1.to_json(), t2.to_json());
self = (t1, t2);
res
}
}

impl TupleThreeJsonMut<T1, T2, T3, +Destruct<T1>, +JsonMut<T1>, +Destruct<T2>, +JsonMut<T2>, +Destruct<T3>, +JsonMut<T3>> of JsonMut<(T1, T2, T3)> {
fn to_json(ref self: (T1, T2, T3)) -> ByteArray {
let (mut t1, mut t2, mut t3) = self;
let res = format!("[{}, {}, {}]", t1.to_json(), t2.to_json(), t3.to_json());
self = (t1, t2, t3);
res
}
}

impl SpanJSON<T, +core::fmt::Display<T>, +Drop<T>, +Copy<T>, +Json<T>, +PartialEq<T>> of Json<Span<T>> {
fn to_json(self: @Span<T>) -> ByteArray {
let self = *self;
let mut json: ByteArray = "";
let mut formatter = Formatter { buffer: json };
write!(formatter, "[").expect('JSON formatting failed');
for value in self {
let value = *value;
write!(formatter, "{}", value.to_json()).expect('JSON formatting failed');
if value != *self.at(self.len() - 1) {
write!(formatter, ", ").expect('JSON formatting failed');
}
};
write!(formatter, "]").expect('JSON formatting failed');
formatter.buffer
}
}

impl SpanJsonMut<T, +core::fmt::Display<T>, +Drop<T>, +Copy<T>, +JsonMut<T>, +PartialEq<T>> of JsonMut<Span<T>> {
fn to_json(ref self: Span<T>) -> ByteArray {
self.to_json()
}
}



impl U256Json = integer_json::IntegerJSON<u256>;
impl U128Json = integer_json::IntegerJSON<u128>;
impl U64Json = integer_json::IntegerJSON<u64>;
impl U32Json = integer_json::IntegerJSON<u32>;
impl U16Json = integer_json::IntegerJSON<u16>;
impl U8Json = integer_json::IntegerJSON<u8>;

pub mod integer_json {
use super::Json;

pub(crate) impl IntegerJSON<T, +core::fmt::Display<T>, +Drop<T>, +Copy<T>> of Json<T> {
fn to_json(self: @T) -> ByteArray {
format!("{}", *self)
}
}
}
Empty file.
Empty file.
Empty file.
60 changes: 60 additions & 0 deletions cairo/kakarot-ssj/py_tests/evm/src/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import json
import re
import subprocess
from typing import Any, Callable, Tuple, Type, Union

import pytest
from py_tests.test_utils.deserializer import Deserializer
from py_tests.test_utils.serializer import Serializer
from py_tests.test_utils.types import ByteArray


@pytest.fixture
def cairo_run() -> Callable[[str, Union[Type[Any], Tuple[Type[Any], ...]], ...], Any]:
def _cairo_run(
function_name: str,
output_type: Union[Type[Any], Tuple[Type[Any], ...]],
*args: Any,
) -> Any:
# Serialize arguments into a compatible format for scarb cairo-run
# JSON encode the serialized arguments - [1,2,3] -> "[1,2,3]"
serialized_args = json.dumps(Serializer.serialize_args(args))

command = [
"scarb",
"pytest",
"-p",
"evm",
"--function",
function_name,
serialized_args,
"--no-build",
]

try:
result = subprocess.run(
command,
cwd="cairo/kakarot-ssj",
capture_output=True,
text=True,
check=True,
)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Command failed with error: {e.stderr}") from e

stdout = result.stdout.strip()
# Extract panic message if present
panic_match = re.search(r"Run panicked with \[\d+ \(\'(.*?)\'\)", stdout)
if panic_match:
raise ValueError(f"Run panicked with: {panic_match.group(1)}")

match = re.search(
r"Run completed successfully, returning (\[.*?\])", result.stdout
)
if not match:
raise ValueError("No array found in the output")

output = ByteArray(json.loads(match.group(1)))
return Deserializer.deserialize(output, output_type)

return _cairo_run
Loading
Loading