litellm/litellm-rust/crates/python-compat/tests/limits.rs
devin-ai-integration[bot] 05d7fb24bd
feat(rust): add python-compat crate for Python data formats (#42510)
* feat(rust): add python-compat crate for Python data formats

Add litellm-python-compat, a PyO3-free crate that reproduces the Python
data formats LiteLLM persists, so Rust readers and writers can interoperate
with state written by the Python proxy:

- literal::literal_eval: a linear recursive-descent port of
  ast.literal_eval (prefixes, escapes, implicit concatenation, numeric
  underscores and radixes, single unary sign, real +/- complex with 3.14
  mixed-mode rules, set(), Python-equality key dedup)
- repr::{repr, to_str}: byte-exact repr()/str(), with a printable table
  generated from CPython's str.isprintable (Unicode 16.0.0)
- json::{dumps, from_json, to_json}: json.dumps defaults and the
  json.loads mapping
- pickle::{loads, dumps}: plain-data pickles via serde-pickle's serde
  interface, which keeps dict insertion order
- truthy::truthy: bool() for plain data

Tests replay fixtures generated by CPython 3.14 (values across every
format and pickle protocol 0-5, plus 154 literal_eval source texts).
Accepted divergences are pinned in a KNOWN table that fails once one
starts matching. A criterion bench covers each format and literal_eval
cost by nesting depth, guarding the linear parse: the py_literal grammar
doubled per nested bracket (105 ms at 16 nested dicts; 19 us at 128 now).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(rust): split python-compat modules and harden the pickle verifier

- Disable class resolution in scripts/verify_rust_pickles.py, and truncate
  the export file once instead of removing and appending to it, so the
  verifier cannot be pointed at a pre-created file whose rows execute code
  through pickle.loads
- Move Error to error.rs and Value to value.rs, leaving lib.rs as the crate
  overview, module list and MAX_DEPTH
- Move the generator and verifier to scripts/, beside the Unicode table
  generator, leaving tests/ to the Rust tests
- Group the bench by measured surface, give every case a Throughput so
  criterion reports bytes per second, and document baseline comparison

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:16:56 -07:00

122 lines
4.2 KiB
Rust

use std::time::{Duration, Instant};
use litellm_python_compat::{Error, MAX_DEPTH, Value, json, literal::literal_eval, pickle};
use rstest::{fixture, rstest};
/// The bracket pair of one container shape, as `(open, close)`.
#[fixture]
fn shapes() -> [(&'static str, &'static str); 3] {
[("[", "]"), ("{'a': ", "}"), ("(", ",)")]
}
fn nested_text(open: &str, close: &str, depth: usize) -> String {
format!("{}1{}", open.repeat(depth), close.repeat(depth))
}
fn nested_list(depth: usize) -> Value {
(0..depth).fold(Value::from(1), |value, _| Value::List(vec![value]))
}
/// A protocol 3 pickle of `depth` nested lists around `1`: `EMPTY_LIST` per level, then
/// `BININT1 1`, then `APPEND` per level. Written by hand because `dumps` refuses the depth.
fn nested_list_pickle(depth: usize) -> Vec<u8> {
let mut data = vec![0x80, 3];
data.extend(std::iter::repeat_n(b']', depth));
data.extend([b'K', 1]);
data.extend(std::iter::repeat_n(b'a', depth));
data.push(b'.');
data
}
#[rstest]
fn literal_eval_accepts_the_limit_and_rejects_past_it(shapes: [(&'static str, &'static str); 3]) {
for (open, close) in shapes {
assert!(literal_eval(&nested_text(open, close, MAX_DEPTH)).is_ok());
assert!(matches!(
literal_eval(&nested_text(open, close, MAX_DEPTH + 1)),
Err(Error::TooDeep)
));
}
}
/// A backtracking parser (the `py_literal` grammar this replaced) doubles per nested level
/// and takes minutes here; the bound is loose enough to survive a slow debug build.
#[rstest]
fn literal_eval_stays_linear_in_depth(shapes: [(&'static str, &'static str); 3]) {
for (open, close) in shapes {
let text = nested_text(open, close, MAX_DEPTH);
let start = Instant::now();
assert!(literal_eval(&text).is_ok());
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(50),
"{open} nested {MAX_DEPTH} deep took {elapsed:?}"
);
}
}
#[rstest]
fn literal_eval_ignores_brackets_inside_strings() {
let text = format!("'{}'", "[".repeat(MAX_DEPTH + 1));
assert!(matches!(literal_eval(&text), Ok(Value::Str(_))));
}
#[rstest]
fn pickle_nesting_is_bounded_in_both_directions() {
assert_eq!(
pickle::loads(&nested_list_pickle(MAX_DEPTH)).unwrap(),
nested_list(MAX_DEPTH)
);
assert!(matches!(
pickle::loads(&nested_list_pickle(MAX_DEPTH + 1)),
Err(Error::InvalidPickle(_))
));
assert!(pickle::dumps(&nested_list(MAX_DEPTH)).is_ok());
assert!(matches!(
pickle::dumps(&nested_list(MAX_DEPTH + 2)),
Err(Error::TooDeep)
));
}
#[rstest]
#[case("{1, 2}", "set")]
#[case("1+2j", "complex")]
fn pickle_dumps_refuses_types_it_would_change(#[case] source: &str, #[case] type_name: &str) {
let value = literal_eval(source).expect("source is a literal");
assert!(matches!(pickle::dumps(&value), Err(Error::NotPicklable(name)) if name == type_name));
}
#[rstest]
#[case(b"\x80\x02c__builtin__\ncomplex\nq\x00.".to_vec(), "a class reference")]
#[case({ let mut data = pickle::dumps(&Value::from(1)).unwrap(); data.push(b'.'); data }, "trailing data")]
fn pickle_loads_rejects(#[case] data: Vec<u8>, #[case] what: &str) {
assert!(
matches!(pickle::loads(&data), Err(Error::InvalidPickle(_))),
"{what} must not decode"
);
}
#[rstest]
#[case(Value::Bytes(b"x".to_vec()), "Object of type bytes is not JSON serializable")]
#[case(Value::Set(vec![Value::from(1)]), "Object of type set is not JSON serializable")]
#[case(Value::Complex { re: 1.0, im: 2.0 }, "Object of type complex is not JSON serializable")]
#[case(
Value::Float(f64::NAN),
"Out of range float values are not JSON compliant"
)]
fn to_json_reports_what_python_json_dumps_would_reject(
#[case] value: Value,
#[case] message: &str,
) {
let error = json::to_json(&value).expect_err("value has no serde_json form");
assert_eq!(error.to_string(), message);
}
/// `json.dumps` writes the non-finite floats that `to_json` cannot represent.
#[rstest]
#[case(f64::NAN, "NaN")]
#[case(f64::INFINITY, "Infinity")]
#[case(f64::NEG_INFINITY, "-Infinity")]
fn json_dumps_writes_non_finite_floats(#[case] value: f64, #[case] text: &str) {
assert_eq!(json::dumps(&Value::Float(value)).unwrap(), text);
}