mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
194 lines
5.7 KiB
Python
194 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
from collections.abc import Callable
|
|
from functools import wraps
|
|
from pathlib import Path
|
|
from types import FunctionType
|
|
from typing import Final, ParamSpec, TypeVar, cast
|
|
|
|
import pytest
|
|
|
|
from tests.sdk_function_trace import (
|
|
FunctionTraceEvent,
|
|
TraceScenario,
|
|
TraceStep,
|
|
assert_function_trace_parity,
|
|
)
|
|
from tests.sdk_function_trace.profiler import _module_qualnames, profile_python
|
|
|
|
_P = ParamSpec("_P")
|
|
_T = TypeVar("_T")
|
|
|
|
|
|
def _passthrough(function: Callable[_P, _T]) -> Callable[_P, _T]:
|
|
@wraps(function)
|
|
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
|
return function(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
class First:
|
|
@staticmethod
|
|
def run() -> None:
|
|
return None
|
|
|
|
|
|
class Second:
|
|
@staticmethod
|
|
def run() -> None:
|
|
return None
|
|
|
|
|
|
class Decorated:
|
|
@_passthrough
|
|
def call(self) -> None:
|
|
return None
|
|
|
|
|
|
def test_source_profiler_qualifies_decorated_methods_by_class() -> None:
|
|
with profile_python(source_root=Path(__file__).parent) as profiler:
|
|
Decorated().call()
|
|
|
|
assert any(event.function.endswith(" Decorated.call") for event in profiler.events)
|
|
assert _module_qualnames(__name__)[cast(FunctionType, Decorated.call.__wrapped__).__code__] == "Decorated.call"
|
|
|
|
|
|
def test_profiler_matches_code_objects_and_keeps_repeated_calls() -> None:
|
|
with profile_python((First.run,)) as profiler:
|
|
Second.run()
|
|
First.run()
|
|
First.run()
|
|
|
|
assert profiler.events == [
|
|
FunctionTraceEvent(function="run", depth=0),
|
|
FunctionTraceEvent(function="run", depth=0),
|
|
]
|
|
|
|
|
|
def test_profiler_records_selected_function_nesting_depth() -> None:
|
|
class Nested:
|
|
@staticmethod
|
|
def run() -> None:
|
|
First.run()
|
|
|
|
with profile_python((Nested.run, First.run)) as profiler:
|
|
Nested.run()
|
|
|
|
assert profiler.events == [
|
|
FunctionTraceEvent(function="run", depth=0),
|
|
FunctionTraceEvent(function="run", depth=1),
|
|
]
|
|
|
|
|
|
def test_profiler_restores_previous_profiler_after_failure() -> None:
|
|
previous: Final = sys.getprofile()
|
|
|
|
with profile_python((First.run,)) as outer:
|
|
with pytest.raises(RuntimeError, match="stop"):
|
|
with profile_python((Second.run,)):
|
|
raise RuntimeError("stop")
|
|
assert sys.getprofile() is outer
|
|
First.run()
|
|
|
|
assert sys.getprofile() is previous
|
|
assert outer.events == [FunctionTraceEvent(function="run", depth=0)]
|
|
|
|
|
|
def test_profiler_does_not_count_coroutine_resumption_as_another_call() -> None:
|
|
async def suspended() -> None:
|
|
await asyncio.sleep(0)
|
|
First.run()
|
|
await asyncio.sleep(0)
|
|
|
|
with profile_python((suspended, First.run)) as profiler:
|
|
asyncio.run(suspended())
|
|
|
|
assert profiler.events == [
|
|
FunctionTraceEvent(function="suspended", depth=0),
|
|
FunctionTraceEvent(function="run", depth=1),
|
|
]
|
|
|
|
|
|
def test_source_profiler_records_real_frame_ancestry() -> None:
|
|
def outer() -> None:
|
|
First.run()
|
|
|
|
with profile_python(source_root=Path(__file__).parent) as profiler:
|
|
outer()
|
|
Second.run()
|
|
|
|
outer_event, first_event, second_event = (
|
|
event for event in profiler.events if event.function.startswith("test_profiler.py:")
|
|
)
|
|
assert first_event.ancestors is not None
|
|
assert outer_event.function in first_event.ancestors
|
|
assert second_event.ancestors is not None
|
|
assert outer_event.function not in second_event.ancestors
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"rust_trace",
|
|
[
|
|
(),
|
|
(FunctionTraceEvent(function="renamed", depth=0),),
|
|
(FunctionTraceEvent(function="run", depth=1),),
|
|
(FunctionTraceEvent(function="run", depth=0),) * 2,
|
|
],
|
|
ids=["missing", "renamed", "wrong-depth", "extra-call"],
|
|
)
|
|
def test_harness_rejects_rust_function_trace_drift(rust_trace: tuple[FunctionTraceEvent, ...]) -> None:
|
|
with pytest.raises(AssertionError, match="Rust function trace differs"):
|
|
assert_function_trace_parity(
|
|
TraceScenario(
|
|
steps=(TraceStep(cast(FunctionType, First.run), depth=0),),
|
|
invoke_python=First.run,
|
|
invoke_rust=lambda: rust_trace,
|
|
)
|
|
)
|
|
|
|
|
|
def test_harness_rejects_python_function_trace_drift() -> None:
|
|
with pytest.raises(AssertionError, match="Python function trace differs"):
|
|
assert_function_trace_parity(
|
|
TraceScenario(
|
|
steps=(TraceStep(cast(FunctionType, First.run), depth=0),),
|
|
invoke_python=Second.run,
|
|
invoke_rust=lambda: (FunctionTraceEvent(function="run", depth=0),),
|
|
)
|
|
)
|
|
|
|
|
|
def test_harness_accepts_matching_traces() -> None:
|
|
assert_function_trace_parity(
|
|
TraceScenario(
|
|
steps=(TraceStep(cast(FunctionType, First.run), depth=0),),
|
|
invoke_python=First.run,
|
|
invoke_rust=lambda: (FunctionTraceEvent(function="run", depth=0),),
|
|
)
|
|
)
|
|
|
|
|
|
def test_harness_rejects_reordered_calls() -> None:
|
|
def begin() -> None:
|
|
return None
|
|
|
|
def finish() -> None:
|
|
return None
|
|
|
|
with pytest.raises(AssertionError, match="Rust function trace differs"):
|
|
assert_function_trace_parity(
|
|
TraceScenario(
|
|
steps=(
|
|
TraceStep(cast(FunctionType, begin), depth=0),
|
|
TraceStep(cast(FunctionType, finish), depth=0),
|
|
),
|
|
invoke_python=lambda: (begin(), finish()),
|
|
invoke_rust=lambda: (
|
|
FunctionTraceEvent(function="finish", depth=0),
|
|
FunctionTraceEvent(function="begin", depth=0),
|
|
),
|
|
)
|
|
)
|