mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
refactor test
This commit is contained in:
parent
993ddf7214
commit
5afda69ca5
13 changed files with 442 additions and 362 deletions
|
|
@ -133,7 +133,7 @@ fixtures. These generic proofs complement, rather than replace, native OCR
|
|||
private proof tests
|
||||
|
||||
The standard-library-only tests in
|
||||
`crates/python-interop/tests/callback_patterns.rs` define small inline Python
|
||||
`crates/python-interop/tests/synthetic/patterns.rs` define small inline Python
|
||||
callbacks, with Rust controlling invocation, ownership and assertions. They
|
||||
compare Python-reference and Rust-retained calls using both direct and awaited
|
||||
invocation. They model the behavior
|
||||
|
|
@ -147,7 +147,7 @@ Existing synthetic lifecycle cases also cover streams, context and cancellation
|
|||
Run this matrix without LiteLLM, vendor SDKs, credentials or services:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path litellm-rust/Cargo.toml -p litellm-python-interop --test callback_patterns
|
||||
cargo test --manifest-path litellm-rust/Cargo.toml -p litellm-python-interop --test synthetic
|
||||
```
|
||||
|
||||
These are behavioral models, not tests of vendor authentication, delivery or
|
||||
|
|
@ -155,6 +155,26 @@ production dispatcher policy. The optional component and integration fixtures
|
|||
exercise existing LiteLLM implementations with fake transports and credentials
|
||||
as supplementary coverage; run them with `make test-rust-python`
|
||||
|
||||
The interop tests have two explicit Cargo targets, each rooted in its directory's
|
||||
`mod.rs`: `tests/synthetic/` for custom, minimal Python implementations and
|
||||
`tests/integration/` for real LiteLLM components with fake transports. The latter
|
||||
are component-level compatibility tests, not complete SDK or proxy route tests.
|
||||
Shared Rust fixtures live in `tests/support/`, and Python scenarios live in
|
||||
`tests/fixtures/`
|
||||
|
||||
Use `#[fixture]` composition for setup: initialize Python once, but create a fresh
|
||||
scenario scope and owner factory for each case. Use named `#[case::behavior]`
|
||||
entries for scenarios and `#[values(Backend::Python, Backend::PreparedCall)]` for
|
||||
the invocation matrix. Keep copying and ownership controls alongside the behavior
|
||||
they distinguish. Register new Rust modules in the appropriate `mod.rs`; Cargo
|
||||
test autodiscovery is disabled so new files cannot silently become a third group
|
||||
|
||||
Run `--test synthetic` for the standard-library-only group. Integration cases
|
||||
remain explicitly ignored without the repository Python environment;
|
||||
`make test-rust-python` configures that environment and runs both groups with
|
||||
`--include-ignored`. To inspect the groups without running them, use
|
||||
`cargo test --manifest-path litellm-rust/Cargo.toml -p litellm-python-interop --tests -- --list`
|
||||
|
||||
The callback lifecycle scenarios use
|
||||
`#[serial(python_interpreter)]` to isolate CPython GC and interpreter-wide
|
||||
LiteLLM settings under `cargo test`. Compatible tests in the same binary use
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ version = "0.1.0"
|
|||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
autotests = false
|
||||
|
||||
[[test]]
|
||||
name = "synthetic"
|
||||
path = "tests/synthetic/mod.rs"
|
||||
|
||||
[[test]]
|
||||
name = "integration"
|
||||
path = "tests/integration/mod.rs"
|
||||
|
||||
[dependencies]
|
||||
pyo3.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use rstest::{fixture, rstest};
|
||||
use serial_test::serial;
|
||||
|
||||
use crate::support::Backend;
|
||||
use crate::support::python::run_fixture;
|
||||
use crate::support::scenarios::{run_scenario_fixture, scenario_scope};
|
||||
|
||||
#[fixture]
|
||||
fn component_scope(scenario_scope: Py<PyDict>) -> Py<PyDict> {
|
||||
Python::attach(|py| {
|
||||
run_fixture(
|
||||
py,
|
||||
scenario_scope.bind(py),
|
||||
include_str!("../fixtures/callback_components.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_components.py"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
scenario_scope
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn integration_scope(scenario_scope: Py<PyDict>) -> Py<PyDict> {
|
||||
Python::attach(|py| {
|
||||
run_fixture(
|
||||
py,
|
||||
scenario_scope.bind(py),
|
||||
include_str!("../fixtures/callback_integrations.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_integrations.py"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
scenario_scope
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::identity_and_ignored_returns("pre_call_identity_and_ignored_returns")]
|
||||
#[case::mutations_visible_to_later_callbacks("pre_call_mutations_visible_to_later_callbacks")]
|
||||
#[case::mutation_survives_failure("pre_call_mutation_survives_failure")]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn pre_call_contract(
|
||||
component_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(component_scope, scenario, backend)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::real_post_call_logging("real_post_call_logging")]
|
||||
#[case::real_post_call_dict_response("real_post_call_dict_response")]
|
||||
#[case::real_sync_logging("real_sync_logging")]
|
||||
#[case::real_sync_logging_hook_failure("real_sync_logging_hook_failure")]
|
||||
#[case::real_sync_failure_chain("real_sync_failure_chain")]
|
||||
#[case::real_async_failure_chain("real_async_failure_chain")]
|
||||
#[case::real_async_logging("real_async_logging")]
|
||||
#[case::real_copy_boundaries("real_copy_boundaries")]
|
||||
#[case::real_logging_worker("real_logging_worker")]
|
||||
#[case::real_sync_stream_copies("real_sync_stream_copies")]
|
||||
#[case::real_stream_completion("real_stream_completion")]
|
||||
#[case::real_stream_close("real_stream_close")]
|
||||
#[case::real_stream_cancellation("real_stream_cancellation")]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn component_contract(
|
||||
component_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(component_scope, scenario, backend)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::real_logging_queue_copy_control("real_logging_queue_copy_control")]
|
||||
#[case::real_crowdstrike_translator_identity("real_crowdstrike_translator_identity")]
|
||||
#[case::real_rubrik_block_lifecycle("real_rubrik_block_lifecycle")]
|
||||
#[case::real_parallel_guardrail_sharing_and_exception_order("real_parallel_guardrail_snapshots")]
|
||||
#[case::real_purview_sync_background_and_active_loop("real_purview_sync_background")]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn integration_contract(
|
||||
integration_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(integration_scope, scenario, backend)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#[path = "../support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
mod lifecycle;
|
||||
mod ocr;
|
||||
175
litellm-rust/crates/python-interop/tests/integration/ocr.rs
Normal file
175
litellm-rust/crates/python-interop/tests/integration/ocr.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use litellm_python_interop::{InvocationMode, InvocationOutcome, PreparedCall};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyList, PyTuple};
|
||||
use rstest::rstest;
|
||||
use serial_test::serial;
|
||||
|
||||
use crate::support::python::{InitializedPython, initialized_python, item, scope};
|
||||
|
||||
fn prepare_pre_call(
|
||||
py: Python<'_>,
|
||||
logger: &Bound<'_, PyAny>,
|
||||
view: &Bound<'_, PyDict>,
|
||||
) -> PyResult<PreparedCall> {
|
||||
let keywords = PyDict::new(py);
|
||||
keywords.set_item("input", "OCR document processing")?;
|
||||
keywords.set_item("api_key", py.None())?;
|
||||
keywords.set_item("additional_args", view)?;
|
||||
Ok(PreparedCall::new(
|
||||
InvocationMode::Direct,
|
||||
logger.getattr("pre_call")?.unbind(),
|
||||
PyTuple::empty(py).unbind(),
|
||||
Some(keywords.unbind()),
|
||||
))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn real_ocr_logging_preserves_execution_roots_and_continues_after_error(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
let _ = initialized_python;
|
||||
Python::attach(|py| {
|
||||
let globals = scope(
|
||||
py,
|
||||
c"
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
class Retain(CustomLogger):
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
order.append('retain')
|
||||
self.view = kwargs['additional_args']
|
||||
self.headers = self.view['headers']
|
||||
self.body = self.view['complete_input_dict']
|
||||
self.snapshot = (self.headers['X-Trace'], self.body['document']['value'])
|
||||
return {'ignored_replacement': True}
|
||||
|
||||
class MutateThenFail(CustomLogger):
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
order.append('mutate_then_fail')
|
||||
view = kwargs['additional_args']
|
||||
view['headers']['X-Trace'] = 'mutated'
|
||||
view['complete_input_dict']['document']['value'] = 'mutated'
|
||||
view['headers'] = {'X-Trace': 'replacement'}
|
||||
view['complete_input_dict'] = {'replacement': True}
|
||||
raise RuntimeError('expected callback failure')
|
||||
|
||||
class Observe(CustomLogger):
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
order.append('observe')
|
||||
self.view = kwargs['additional_args']
|
||||
self.snapshot = (
|
||||
tuple(sorted(self.view['headers'].items())),
|
||||
self.view['complete_input_dict'].get('replacement'),
|
||||
'document' in self.view['complete_input_dict'],
|
||||
)
|
||||
|
||||
",
|
||||
)?;
|
||||
let order = PyList::empty(py);
|
||||
globals.set_item("order", &order)?;
|
||||
let first = item(&globals, "Retain").call0()?;
|
||||
let last = item(&globals, "Observe").call0()?;
|
||||
let document = PyDict::new(py);
|
||||
document.set_item("value", "original")?;
|
||||
let headers = PyDict::new(py);
|
||||
headers.set_item("X-Trace", "original")?;
|
||||
let body = PyDict::new(py);
|
||||
body.set_item("document", &document)?;
|
||||
body.set_item("alias", &document)?;
|
||||
let view = PyDict::new(py);
|
||||
view.set_item("headers", &headers)?;
|
||||
view.set_item("complete_input_dict", &body)?;
|
||||
view.set_item("api_base", "https://example.invalid/ocr")?;
|
||||
let keywords = PyDict::new(py);
|
||||
keywords.set_item("model", "test")?;
|
||||
keywords.set_item("messages", PyList::empty(py))?;
|
||||
keywords.set_item("stream", false)?;
|
||||
keywords.set_item("call_type", "ocr")?;
|
||||
keywords.set_item(
|
||||
"start_time",
|
||||
py.import("datetime")?
|
||||
.getattr("datetime")?
|
||||
.call_method0("now")?,
|
||||
)?;
|
||||
keywords.set_item("litellm_call_id", "retained-test")?;
|
||||
keywords.set_item("function_id", "retained-test")?;
|
||||
keywords.set_item(
|
||||
"dynamic_input_callbacks",
|
||||
PyList::new(
|
||||
py,
|
||||
[&first, &item(&globals, "MutateThenFail").call0()?, &last],
|
||||
)?,
|
||||
)?;
|
||||
let logger = py
|
||||
.import("litellm.litellm_core_utils.litellm_logging")?
|
||||
.getattr("Logging")?
|
||||
.call((), Some(&keywords))?;
|
||||
let invocation = prepare_pre_call(py, &logger, &view)?;
|
||||
match invocation.invoke(py)? {
|
||||
InvocationOutcome::Returned(value) => assert!(value.is_none(py)),
|
||||
InvocationOutcome::Awaitable(_) => {
|
||||
panic!("direct binding produced an awaitable outcome")
|
||||
}
|
||||
}
|
||||
drop(invocation);
|
||||
assert!(headers.is(first.getattr("headers")?));
|
||||
assert!(body.is(first.getattr("body")?));
|
||||
assert!(view.is(last.getattr("view")?));
|
||||
assert_eq!(item(&headers, "X-Trace").extract::<String>()?, "mutated");
|
||||
assert_eq!(
|
||||
order.extract::<Vec<String>>()?,
|
||||
["retain", "mutate_then_fail", "observe"]
|
||||
);
|
||||
assert_eq!(
|
||||
first.getattr("snapshot")?.extract::<(String, String)>()?,
|
||||
("original".to_owned(), "original".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
last.getattr("snapshot")?
|
||||
.extract::<(Vec<(String, String)>, bool, bool)>()?,
|
||||
(
|
||||
vec![("X-Trace".to_owned(), "replacement".to_owned())],
|
||||
true,
|
||||
false
|
||||
)
|
||||
);
|
||||
assert!(first.getattr("view")?.is(last.getattr("view")?));
|
||||
assert!(first.getattr("body")?.get_item("document")?.is(&document));
|
||||
assert!(first.getattr("body")?.get_item("alias")?.is(&document));
|
||||
assert_eq!(item(&document, "value").extract::<String>()?, "mutated");
|
||||
assert_eq!(
|
||||
last.getattr("view")?
|
||||
.get_item("headers")?
|
||||
.get_item("X-Trace")?
|
||||
.extract::<String>()?,
|
||||
"replacement"
|
||||
);
|
||||
let replacement = PyDict::new(py);
|
||||
replacement.set_item("replacement", true)?;
|
||||
assert!(
|
||||
last.getattr("view")?
|
||||
.get_item("complete_input_dict")?
|
||||
.eq(replacement)?
|
||||
);
|
||||
document.set_item("value", "after invocation")?;
|
||||
assert_eq!(
|
||||
first
|
||||
.getattr("body")?
|
||||
.get_item("document")?
|
||||
.get_item("value")?
|
||||
.extract::<String>()?,
|
||||
"after invocation"
|
||||
);
|
||||
drop((headers, body, view));
|
||||
assert_eq!(
|
||||
first
|
||||
.getattr("headers")?
|
||||
.get_item("X-Trace")?
|
||||
.extract::<String>()?,
|
||||
"mutated"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
@ -1 +1,9 @@
|
|||
mod callback_owner;
|
||||
pub mod python;
|
||||
pub mod scenarios;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Backend {
|
||||
Python,
|
||||
PreparedCall,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use rstest::fixture;
|
||||
|
||||
use super::python::{InitializedPython, initialized_python, run_fixture};
|
||||
use super::{Backend, callback_owner};
|
||||
|
||||
#[fixture]
|
||||
pub fn scenario_scope(initialized_python: &InitializedPython) -> Py<PyDict> {
|
||||
initialized_python.attach(|py| {
|
||||
let globals = PyDict::new(py);
|
||||
globals
|
||||
.set_item(
|
||||
"factory",
|
||||
Py::new(py, callback_owner::OwnerFactory::default()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
globals
|
||||
.set_item(
|
||||
"AWAIT_ADAPTER_FILENAME",
|
||||
litellm_python_interop::AWAIT_ADAPTER_FILENAME
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
run_fixture(
|
||||
py,
|
||||
&globals,
|
||||
include_str!("../fixtures/callback_lifecycle.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_lifecycle.py"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
globals.unbind()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_scenario_fixture(
|
||||
scenario_scope: Py<PyDict>,
|
||||
scenario: &str,
|
||||
backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
Python::attach(|py| {
|
||||
let globals = scenario_scope.bind(py);
|
||||
globals.get_item("run_scenario")?.unwrap().call1((
|
||||
scenario,
|
||||
matches!(backend, Backend::PreparedCall),
|
||||
globals.get_item("factory")?.unwrap(),
|
||||
))?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
@ -6,10 +6,8 @@ use pyo3::types::{PyDict, PyTuple};
|
|||
use rstest::rstest;
|
||||
use serial_test::serial;
|
||||
|
||||
#[path = "support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
use support::python::{InitializedPython, initialized_python, item, scope};
|
||||
use crate::support::Backend;
|
||||
use crate::support::python::{InitializedPython, initialized_python, item, scope};
|
||||
|
||||
enum ControlCall {
|
||||
Reference(Py<PyAny>),
|
||||
|
|
@ -22,10 +20,10 @@ impl ControlCall {
|
|||
callback: Bound<'_, PyAny>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Option<Bound<'_, PyDict>>,
|
||||
retained: bool,
|
||||
backend: Backend,
|
||||
mode: InvocationMode,
|
||||
) -> PyResult<Self> {
|
||||
if retained {
|
||||
if matches!(backend, Backend::PreparedCall) {
|
||||
return Ok(Self::Prepared(PreparedCall::new(
|
||||
mode,
|
||||
callback.unbind(),
|
||||
|
|
@ -76,7 +74,7 @@ fn argument_copy_boundaries_determine_identity_and_mutation_visibility(
|
|||
#[case] transform: &CStr,
|
||||
#[case] expected_identity: (bool, bool, bool),
|
||||
#[case] live_stages: (u8, u8, u8),
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
#[values(InvocationMode::Direct, InvocationMode::Await)] mode: InvocationMode,
|
||||
) -> PyResult<()> {
|
||||
initialized_python.attach(|py| {
|
||||
|
|
@ -110,7 +108,7 @@ async def observe_async(value, *, alias):
|
|||
),
|
||||
transformed.get_item(0)?.cast_into::<PyTuple>()?,
|
||||
Some(transformed.get_item(1)?.cast_into::<PyDict>()?),
|
||||
retained,
|
||||
backend,
|
||||
mode,
|
||||
)?;
|
||||
let original = item(&globals, "original");
|
||||
|
|
@ -163,7 +161,7 @@ fn result_copy_boundaries_determine_root_and_nested_identity(
|
|||
initialized_python: &InitializedPython,
|
||||
#[case] transform: Option<&CStr>,
|
||||
#[case] expected_identity: (bool, bool),
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
#[values(InvocationMode::Direct, InvocationMode::Await)] mode: InvocationMode,
|
||||
) -> PyResult<()> {
|
||||
initialized_python.attach(|py| {
|
||||
|
|
@ -194,7 +192,7 @@ async def callback_async():
|
|||
),
|
||||
PyTuple::empty(py),
|
||||
None,
|
||||
retained,
|
||||
backend,
|
||||
mode,
|
||||
)?;
|
||||
let pending = call.invoke(py, mode)?;
|
||||
|
|
@ -1,36 +1,51 @@
|
|||
use std::process::Command;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use litellm_python_interop::{InvocationMode, InvocationOutcome, PreparedCall};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyTuple};
|
||||
use rstest::{fixture, rstest};
|
||||
use rstest::rstest;
|
||||
use serial_test::{parallel, serial};
|
||||
|
||||
#[path = "support/callback_owner.rs"]
|
||||
mod callback_owner;
|
||||
|
||||
#[path = "support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
use support::python::{InitializedPython, initialized_python, item, run_fixture};
|
||||
use crate::support::Backend;
|
||||
use crate::support::python::{InitializedPython, initialized_python, item, run_fixture};
|
||||
use crate::support::scenarios::{run_scenario_fixture, scenario_scope};
|
||||
|
||||
#[test]
|
||||
fn cold_awaited_adapter_initialization_allows_reentry() -> PyResult<()> {
|
||||
let test = "cold_awaited_adapter_initialization_allows_reentry";
|
||||
let test = concat!(
|
||||
module_path!(),
|
||||
"::cold_awaited_adapter_initialization_allows_reentry"
|
||||
)
|
||||
.split_once("::")
|
||||
.unwrap()
|
||||
.1;
|
||||
let child_env = "LITELLM_INTEROP_COLD_REENTRY_CHILD";
|
||||
if std::env::var(child_env).as_deref() != Ok(test) {
|
||||
let mut child = Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", test, "--nocapture"])
|
||||
.env(child_env, test)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(15);
|
||||
loop {
|
||||
if let Some(status) = child.try_wait().unwrap() {
|
||||
let mut output = String::new();
|
||||
child
|
||||
.stdout
|
||||
.take()
|
||||
.unwrap()
|
||||
.read_to_string(&mut output)
|
||||
.unwrap();
|
||||
assert!(
|
||||
status.success(),
|
||||
"awaited adapter reentry child failed: {status}"
|
||||
"awaited adapter reentry child failed: {status}\n{output}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("test result: ok. 1 passed; 0 failed; 0 ignored;"),
|
||||
"awaited adapter reentry child did not run exactly one test:\n{output}"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -54,39 +69,6 @@ fn cold_awaited_adapter_initialization_allows_reentry() -> PyResult<()> {
|
|||
})
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn scenario_scope(initialized_python: &InitializedPython) -> Py<PyDict> {
|
||||
let _ = initialized_python;
|
||||
Python::attach(|py| {
|
||||
let globals = PyDict::new(py);
|
||||
globals
|
||||
.set_item(
|
||||
"factory",
|
||||
Py::new(py, callback_owner::OwnerFactory::default()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
globals
|
||||
.set_item(
|
||||
"AWAIT_ADAPTER_FILENAME",
|
||||
litellm_python_interop::AWAIT_ADAPTER_FILENAME
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
run_fixture(
|
||||
py,
|
||||
&globals,
|
||||
include_str!("fixtures/callback_lifecycle.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_lifecycle.py"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
globals.unbind()
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::awaitable_kinds("awaitable_kinds")]
|
||||
#[case::identity_and_context("identity_and_context")]
|
||||
|
|
@ -106,116 +88,9 @@ fn scenario_scope(initialized_python: &InitializedPython) -> Py<PyDict> {
|
|||
fn lifecycle_contract(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(scenario_scope, scenario, retained, None)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::identity_and_ignored_returns("pre_call_identity_and_ignored_returns")]
|
||||
#[case::mutations_visible_to_later_callbacks("pre_call_mutations_visible_to_later_callbacks")]
|
||||
#[case::mutation_survives_failure("pre_call_mutation_survives_failure")]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn pre_call_contract(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(false, true)] retained: bool,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(
|
||||
scenario_scope,
|
||||
scenario,
|
||||
retained,
|
||||
Some((
|
||||
include_str!("fixtures/callback_components.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_components.py"
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::real_post_call_logging("real_post_call_logging")]
|
||||
#[case::real_post_call_dict_response("real_post_call_dict_response")]
|
||||
#[case::real_sync_logging("real_sync_logging")]
|
||||
#[case::real_sync_logging_hook_failure("real_sync_logging_hook_failure")]
|
||||
#[case::real_sync_failure_chain("real_sync_failure_chain")]
|
||||
#[case::real_async_failure_chain("real_async_failure_chain")]
|
||||
#[case::real_async_logging("real_async_logging")]
|
||||
#[case::real_copy_boundaries("real_copy_boundaries")]
|
||||
#[case::real_logging_worker("real_logging_worker")]
|
||||
#[case::real_sync_stream_copies("real_sync_stream_copies")]
|
||||
#[case::real_stream_completion("real_stream_completion")]
|
||||
#[case::real_stream_close("real_stream_close")]
|
||||
#[case::real_stream_cancellation("real_stream_cancellation")]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn component_contract(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(false, true)] retained: bool,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(
|
||||
scenario_scope,
|
||||
scenario,
|
||||
retained,
|
||||
Some((
|
||||
include_str!("fixtures/callback_components.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_components.py"
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::real_logging_queue_copy_control("real_logging_queue_copy_control")]
|
||||
#[case::real_crowdstrike_translator_identity("real_crowdstrike_translator_identity")]
|
||||
#[case::real_rubrik_block_lifecycle("real_rubrik_block_lifecycle")]
|
||||
#[case::real_parallel_guardrail_sharing_and_exception_order("real_parallel_guardrail_snapshots")]
|
||||
#[case::real_purview_sync_background_and_active_loop("real_purview_sync_background")]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
#[serial(python_interpreter)]
|
||||
fn integration_contract(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[case] scenario: &str,
|
||||
#[values(false, true)] retained: bool,
|
||||
) -> PyResult<()> {
|
||||
run_scenario_fixture(
|
||||
scenario_scope,
|
||||
scenario,
|
||||
retained,
|
||||
Some((
|
||||
include_str!("fixtures/callback_integrations.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_integrations.py"
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_scenario_fixture(
|
||||
scenario_scope: Py<PyDict>,
|
||||
scenario: &str,
|
||||
retained: bool,
|
||||
fixture: Option<(&str, &str)>,
|
||||
) -> PyResult<()> {
|
||||
Python::attach(|py| {
|
||||
let globals = scenario_scope.bind(py);
|
||||
if let Some((source, filename)) = fixture {
|
||||
run_fixture(py, globals, source, filename)?;
|
||||
}
|
||||
globals.get_item("run_scenario")?.unwrap().call1((
|
||||
scenario,
|
||||
retained,
|
||||
globals.get_item("factory")?.unwrap(),
|
||||
))?;
|
||||
Ok(())
|
||||
})
|
||||
run_scenario_fixture(scenario_scope, scenario, backend)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
|
@ -229,14 +104,12 @@ fn control_contract(
|
|||
scenario_scope: Py<PyDict>,
|
||||
#[case] witness: &str,
|
||||
#[case] control: &str,
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
#[values(false, true)] awaited: bool,
|
||||
) -> PyResult<()> {
|
||||
run_control_fixture(scenario_scope, witness, control, retained, awaited)
|
||||
run_control_fixture(scenario_scope, witness, control, backend, awaited)
|
||||
}
|
||||
|
||||
// The `weak` control wraps nothing: it holds only weak references and never
|
||||
// calls the factory, so the `retained` axis has no effect on it.
|
||||
#[rstest]
|
||||
#[case::expired_borrow("deferred_lifetime")]
|
||||
#[case::externally_owned_borrow("borrowed_lifetime")]
|
||||
|
|
@ -246,7 +119,7 @@ fn weak_control(
|
|||
#[case] witness: &str,
|
||||
#[values(false, true)] awaited: bool,
|
||||
) -> PyResult<()> {
|
||||
run_control_fixture(scenario_scope, witness, "weak", false, awaited)
|
||||
run_control_fixture(scenario_scope, witness, "weak", Backend::Python, awaited)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
|
@ -256,16 +129,16 @@ fn weak_control(
|
|||
fn pending_handoff_control(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[case] control: &str,
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
run_control_fixture(scenario_scope, "pending_handoff", control, retained, true)
|
||||
run_control_fixture(scenario_scope, "pending_handoff", control, backend, true)
|
||||
}
|
||||
|
||||
fn run_control_fixture(
|
||||
scenario_scope: Py<PyDict>,
|
||||
witness: &str,
|
||||
control: &str,
|
||||
retained: bool,
|
||||
backend: Backend,
|
||||
awaited: bool,
|
||||
) -> PyResult<()> {
|
||||
Python::attach(|py| {
|
||||
|
|
@ -273,7 +146,7 @@ fn run_control_fixture(
|
|||
run_fixture(
|
||||
py,
|
||||
globals,
|
||||
include_str!("fixtures/callback_controls.py"),
|
||||
include_str!("../fixtures/callback_controls.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_controls.py"
|
||||
|
|
@ -282,7 +155,7 @@ fn run_control_fixture(
|
|||
globals.get_item("run_control")?.unwrap().call1((
|
||||
witness,
|
||||
control,
|
||||
retained,
|
||||
matches!(backend, Backend::PreparedCall),
|
||||
awaited,
|
||||
globals.get_item("factory")?.unwrap(),
|
||||
))?;
|
||||
|
|
@ -295,10 +168,10 @@ fn invoke_direct_callback(
|
|||
globals: &Bound<'_, PyDict>,
|
||||
callback: &str,
|
||||
argument: &str,
|
||||
retained: bool,
|
||||
backend: Backend,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let args = PyTuple::new(py, [item(globals, argument)])?;
|
||||
if !retained {
|
||||
if matches!(backend, Backend::Python) {
|
||||
let factory = item(globals, "ReferenceFactory").call0()?;
|
||||
let owner = factory.call_method1("prepare", (item(globals, callback), args))?;
|
||||
let result = owner.call_method0("invoke");
|
||||
|
|
@ -322,7 +195,7 @@ fn invoke_direct_callback(
|
|||
#[serial(python_interpreter)]
|
||||
fn retained_field_survives_replacement_and_observes_original_mutations(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
Python::attach(|py| {
|
||||
let globals = scenario_scope.bind(py);
|
||||
|
|
@ -344,7 +217,7 @@ def replace(value):
|
|||
None,
|
||||
)?;
|
||||
for callback in ["retain", "replace"] {
|
||||
assert!(invoke_direct_callback(py, globals, callback, "event", retained)?.is_none(py));
|
||||
assert!(invoke_direct_callback(py, globals, callback, "event", backend)?.is_none(py));
|
||||
}
|
||||
let original = item(globals, "original");
|
||||
let replacement = item(globals, "replacement");
|
||||
|
|
@ -390,7 +263,7 @@ def replace(value):
|
|||
#[serial(python_interpreter)]
|
||||
fn queued_graph_outlives_invocation_and_stays_live_until_serialized(
|
||||
scenario_scope: Py<PyDict>,
|
||||
#[values(false, true)] retained: bool,
|
||||
#[values(Backend::Python, Backend::PreparedCall)] backend: Backend,
|
||||
) -> PyResult<()> {
|
||||
Python::attach(|py| {
|
||||
let globals = scenario_scope.bind(py);
|
||||
|
|
@ -408,7 +281,7 @@ def enqueue(value):
|
|||
Some(globals),
|
||||
None,
|
||||
)?;
|
||||
assert!(invoke_direct_callback(py, globals, "enqueue", "payload", retained)?.is_none(py));
|
||||
assert!(invoke_direct_callback(py, globals, "enqueue", "payload", backend)?.is_none(py));
|
||||
py.run(c"del sentinel, payload\ngc.collect()", Some(globals), None)?;
|
||||
let reference = item(globals, "reference");
|
||||
assert!(!reference.call0()?.is_none());
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#[path = "../support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
mod controls;
|
||||
mod lifecycle;
|
||||
mod patterns;
|
||||
mod prepared_call;
|
||||
mod primitives;
|
||||
|
|
@ -6,16 +6,8 @@ use pyo3::types::{PyDict, PyTuple};
|
|||
use rstest::rstest;
|
||||
use serial_test::serial;
|
||||
|
||||
#[path = "support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
use support::python::{InitializedPython, initialized_python, item, run_fixture, scope};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Backend {
|
||||
Python,
|
||||
PreparedCall,
|
||||
}
|
||||
use crate::support::Backend;
|
||||
use crate::support::python::{InitializedPython, initialized_python, item, run_fixture, scope};
|
||||
|
||||
#[pyfunction]
|
||||
fn invoke_prepared<'py>(
|
||||
|
|
@ -3,13 +3,12 @@ use pyo3::exceptions::{PyAssertionError, PyKeyboardInterrupt, PyValueError};
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyList, PyTuple};
|
||||
use rstest::rstest;
|
||||
use serial_test::parallel;
|
||||
|
||||
#[path = "support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
use support::python::{InitializedPython, initialized_python, item, run_fixture, scope};
|
||||
use crate::support::python::{InitializedPython, initialized_python, item, run_fixture, scope};
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn retains_aliases_mutations_and_original_result(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
|
|
@ -61,6 +60,7 @@ def callback(data, *, alias):
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn preserves_exception_identity_cause_traceback_and_prior_mutation(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
|
|
@ -105,6 +105,7 @@ def callback(data):
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn returns_coroutine_without_executing_it(initialized_python: &InitializedPython) -> PyResult<()> {
|
||||
let _ = initialized_python;
|
||||
Python::attach(|py| {
|
||||
|
|
@ -152,6 +153,7 @@ fn reenter(py: Python<'_>, callback: Py<PyAny>, payload: Py<PyAny>) -> PyResult<
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn preserves_current_context_thread_and_reentry(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
|
|
@ -208,6 +210,7 @@ def outer():
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn owns_arguments_until_release_and_preserves_callback_retention(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
|
|
@ -269,24 +272,8 @@ class Callback:
|
|||
})
|
||||
}
|
||||
|
||||
fn prepare_pre_call(
|
||||
py: Python<'_>,
|
||||
logger: &Bound<'_, PyAny>,
|
||||
view: &Bound<'_, PyDict>,
|
||||
) -> PyResult<PreparedCall> {
|
||||
let keywords = PyDict::new(py);
|
||||
keywords.set_item("input", "OCR document processing")?;
|
||||
keywords.set_item("api_key", py.None())?;
|
||||
keywords.set_item("additional_args", view)?;
|
||||
Ok(PreparedCall::new(
|
||||
InvocationMode::Direct,
|
||||
logger.getattr("pre_call")?.unbind(),
|
||||
PyTuple::empty(py).unbind(),
|
||||
Some(keywords.unbind()),
|
||||
))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn checked_runner_rejects_unhandled_background_failures(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
|
|
@ -296,7 +283,7 @@ fn checked_runner_rejects_unhandled_background_failures(
|
|||
run_fixture(
|
||||
py,
|
||||
&globals,
|
||||
include_str!("fixtures/callback_lifecycle.py"),
|
||||
include_str!("../fixtures/callback_lifecycle.py"),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/callback_lifecycle.py"
|
||||
|
|
@ -355,151 +342,6 @@ async def scenario(cyclic, handled, observed):
|
|||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[ignore = "requires the repository Python environment and LiteLLM on PYTHONPATH"]
|
||||
fn real_ocr_logging_preserves_execution_roots_and_continues_after_error(
|
||||
initialized_python: &InitializedPython,
|
||||
) -> PyResult<()> {
|
||||
let _ = initialized_python;
|
||||
Python::attach(|py| {
|
||||
let globals = scope(
|
||||
py,
|
||||
c"
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
class Retain(CustomLogger):
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
order.append('retain')
|
||||
self.view = kwargs['additional_args']
|
||||
self.headers = self.view['headers']
|
||||
self.body = self.view['complete_input_dict']
|
||||
self.snapshot = (self.headers['X-Trace'], self.body['document']['value'])
|
||||
return {'ignored_replacement': True}
|
||||
|
||||
class MutateThenFail(CustomLogger):
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
order.append('mutate_then_fail')
|
||||
view = kwargs['additional_args']
|
||||
view['headers']['X-Trace'] = 'mutated'
|
||||
view['complete_input_dict']['document']['value'] = 'mutated'
|
||||
view['headers'] = {'X-Trace': 'replacement'}
|
||||
view['complete_input_dict'] = {'replacement': True}
|
||||
raise RuntimeError('expected callback failure')
|
||||
|
||||
class Observe(CustomLogger):
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
order.append('observe')
|
||||
self.view = kwargs['additional_args']
|
||||
self.snapshot = (
|
||||
tuple(sorted(self.view['headers'].items())),
|
||||
self.view['complete_input_dict'].get('replacement'),
|
||||
'document' in self.view['complete_input_dict'],
|
||||
)
|
||||
|
||||
",
|
||||
)?;
|
||||
let order = PyList::empty(py);
|
||||
globals.set_item("order", &order)?;
|
||||
let first = item(&globals, "Retain").call0()?;
|
||||
let last = item(&globals, "Observe").call0()?;
|
||||
let document = PyDict::new(py);
|
||||
document.set_item("value", "original")?;
|
||||
let headers = PyDict::new(py);
|
||||
headers.set_item("X-Trace", "original")?;
|
||||
let body = PyDict::new(py);
|
||||
body.set_item("document", &document)?;
|
||||
body.set_item("alias", &document)?;
|
||||
let view = PyDict::new(py);
|
||||
view.set_item("headers", &headers)?;
|
||||
view.set_item("complete_input_dict", &body)?;
|
||||
view.set_item("api_base", "https://example.invalid/ocr")?;
|
||||
let keywords = PyDict::new(py);
|
||||
keywords.set_item("model", "test")?;
|
||||
keywords.set_item("messages", PyList::empty(py))?;
|
||||
keywords.set_item("stream", false)?;
|
||||
keywords.set_item("call_type", "ocr")?;
|
||||
keywords.set_item(
|
||||
"start_time",
|
||||
py.import("datetime")?
|
||||
.getattr("datetime")?
|
||||
.call_method0("now")?,
|
||||
)?;
|
||||
keywords.set_item("litellm_call_id", "retained-test")?;
|
||||
keywords.set_item("function_id", "retained-test")?;
|
||||
keywords.set_item(
|
||||
"dynamic_input_callbacks",
|
||||
PyList::new(
|
||||
py,
|
||||
[&first, &item(&globals, "MutateThenFail").call0()?, &last],
|
||||
)?,
|
||||
)?;
|
||||
let logger = py
|
||||
.import("litellm.litellm_core_utils.litellm_logging")?
|
||||
.getattr("Logging")?
|
||||
.call((), Some(&keywords))?;
|
||||
let invocation = prepare_pre_call(py, &logger, &view)?;
|
||||
assert!(invoke_direct(&invocation, py)?.is_none(py));
|
||||
drop(invocation);
|
||||
assert!(headers.is(first.getattr("headers")?));
|
||||
assert!(body.is(first.getattr("body")?));
|
||||
assert!(view.is(last.getattr("view")?));
|
||||
assert_eq!(item(&headers, "X-Trace").extract::<String>()?, "mutated");
|
||||
assert_eq!(
|
||||
order.extract::<Vec<String>>()?,
|
||||
["retain", "mutate_then_fail", "observe"]
|
||||
);
|
||||
assert_eq!(
|
||||
first.getattr("snapshot")?.extract::<(String, String)>()?,
|
||||
("original".to_owned(), "original".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
last.getattr("snapshot")?
|
||||
.extract::<(Vec<(String, String)>, bool, bool)>()?,
|
||||
(
|
||||
vec![("X-Trace".to_owned(), "replacement".to_owned())],
|
||||
true,
|
||||
false
|
||||
)
|
||||
);
|
||||
assert!(first.getattr("view")?.is(last.getattr("view")?));
|
||||
assert!(first.getattr("body")?.get_item("document")?.is(&document));
|
||||
assert!(first.getattr("body")?.get_item("alias")?.is(&document));
|
||||
assert_eq!(item(&document, "value").extract::<String>()?, "mutated");
|
||||
assert_eq!(
|
||||
last.getattr("view")?
|
||||
.get_item("headers")?
|
||||
.get_item("X-Trace")?
|
||||
.extract::<String>()?,
|
||||
"replacement"
|
||||
);
|
||||
let replacement = PyDict::new(py);
|
||||
replacement.set_item("replacement", true)?;
|
||||
assert!(
|
||||
last.getattr("view")?
|
||||
.get_item("complete_input_dict")?
|
||||
.eq(replacement)?
|
||||
);
|
||||
document.set_item("value", "after invocation")?;
|
||||
assert_eq!(
|
||||
first
|
||||
.getattr("body")?
|
||||
.get_item("document")?
|
||||
.get_item("value")?
|
||||
.extract::<String>()?,
|
||||
"after invocation"
|
||||
);
|
||||
drop((headers, body, view));
|
||||
assert_eq!(
|
||||
first
|
||||
.getattr("headers")?
|
||||
.get_item("X-Trace")?
|
||||
.extract::<String>()?,
|
||||
"mutated"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn invoke_direct(call: &PreparedCall, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
match call.invoke(py)? {
|
||||
InvocationOutcome::Returned(value) => Ok(value),
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
use serial_test::parallel;
|
||||
|
||||
use litellm_python_interop::{from_py, release_count, release_gil, to_py};
|
||||
|
||||
#[path = "support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
use support::python::{InitializedPython, initialized_python};
|
||||
use crate::support::python::{InitializedPython, initialized_python};
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn serde_values_round_trip_through_python(#[from(initialized_python)] python: &InitializedPython) {
|
||||
python.attach(|py| {
|
||||
let expected = json!({"model": "test", "items": [1, true, null]});
|
||||
|
|
@ -21,6 +20,7 @@ fn serde_values_round_trip_through_python(#[from(initialized_python)] python: &I
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[parallel(python_interpreter)]
|
||||
fn release_gil_runs_work_and_records_it(#[from(initialized_python)] python: &InitializedPython) {
|
||||
let before = release_count();
|
||||
let result = python.attach(|py| release_gil(py, || 42));
|
||||
Loading…
Add table
Reference in a new issue