litellm/tests/sdk_function_trace/test_profiler.py
yujonglee 198906495f
refactor(python-bridge): split routes and add shared function tracing (#39031)
* refactor(python-bridge): split non-streaming bridge modules

* refactor(python-bridge): bring shared function tracing into route layer

* feat(dev): list Python route functions and call sites

* feat(dev): list Rust route functions and call sites

* docs(dev): record OCR parity gaps across Python and Rust

* feat(dev): list executed SDK calls with runtime tracing

* feat(dev): report Python vs Rust SDK pipeline steps in one CLI

* feat(dev): side-by-side pipeline step report in compare CLI

* fix(dev): drop invalid Final annotations in compare cell loop

* feat(dev): blue python-only and yellow rust-only steps in compare CLI

* feat(dev): vertical layout with section spacing in compare CLI

* fix(dev): validate SDK trace stages across sync and async routes

* refactor(rust): align SDK route call structure with Python

* refactor(python-bridge): share sync and async route call wrappers

* refactor(dev): split compare CLI into fixtures, runtime, and report modules

* fix(ci): run SDK trace tests and satisfy test lint
2026-09-02 16:26:35 -07:00

167 lines
4.9 KiB
Python

from __future__ import annotations
import asyncio
import sys
from pathlib import Path
from types import FunctionType
from typing import Final, cast
import pytest
from tests.sdk_function_trace import (
FunctionTraceEvent,
TraceScenario,
TraceStep,
assert_function_trace_parity,
)
from tests.sdk_function_trace.profiler import profile_python
class First:
@staticmethod
def run() -> None:
return None
class Second:
@staticmethod
def run() -> None:
return None
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),
),
)
)