205 lines
6.1 KiB
Python
205 lines
6.1 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Tests for bench engine progress monitor."""
|
|
|
|
# Standard
|
|
import io
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
# First Party
|
|
from lmcache.cli.commands.bench.engine_bench.progress import (
|
|
ProgressMonitor,
|
|
)
|
|
from lmcache.cli.commands.bench.engine_bench.stats import (
|
|
RequestResult,
|
|
StatsCollector,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_collector() -> StatsCollector:
|
|
return StatsCollector()
|
|
|
|
|
|
def _make_result(request_id: str = "req_0") -> RequestResult:
|
|
now = time.time()
|
|
return RequestResult(
|
|
request_id=request_id,
|
|
successful=True,
|
|
ttft=0.3,
|
|
request_latency=2.0,
|
|
num_input_tokens=10000,
|
|
num_output_tokens=128,
|
|
decode_speed=48.0,
|
|
submit_time=now,
|
|
first_token_time=now + 0.3,
|
|
finish_time=now + 2.0,
|
|
error="",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State tracking
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestProgressMonitorState:
|
|
def test_initial_state(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
assert m._in_flight == 0
|
|
assert m._log_messages == []
|
|
|
|
def test_in_flight_tracking(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
m.on_request_sent("r0")
|
|
m.on_request_sent("r1")
|
|
m.on_request_sent("r2")
|
|
assert m._in_flight == 3
|
|
m.on_request_finished("r0", True)
|
|
m.on_request_finished("r1", False)
|
|
assert m._in_flight == 1
|
|
|
|
def test_on_request_finished_decrements_to_zero(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
m.on_request_sent("r0")
|
|
m.on_request_finished("r0", True)
|
|
assert m._in_flight == 0
|
|
|
|
def test_on_request_finished_does_not_go_negative(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
m.on_request_finished("r0", True)
|
|
assert m._in_flight == 0
|
|
|
|
def test_multiple_log_messages(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
m.log_message("first")
|
|
m.log_message("second")
|
|
m.log_message("third")
|
|
assert m._log_messages == ["first", "second", "third"]
|
|
|
|
def test_log_message_max_lines(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
for i in range(10):
|
|
m.log_message(f"msg_{i}")
|
|
assert len(m._log_messages) == ProgressMonitor.LOG_LINES
|
|
assert m._log_messages[0] == "msg_5"
|
|
assert m._log_messages[-1] == "msg_9"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Quiet mode
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestProgressMonitorQuiet:
|
|
def test_quiet_no_thread(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
m.start()
|
|
assert m._thread is None
|
|
|
|
def test_quiet_stop_safe(self) -> None:
|
|
m = ProgressMonitor(_make_collector(), quiet=True)
|
|
m.start()
|
|
m.stop() # should not raise
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thread lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestProgressMonitorThread:
|
|
def test_start_stop_lifecycle(self) -> None:
|
|
collector = _make_collector()
|
|
# Redirect stdout to suppress ANSI output
|
|
old_stdout = sys.stdout
|
|
sys.stdout = io.StringIO()
|
|
try:
|
|
m = ProgressMonitor(collector)
|
|
m.start()
|
|
assert m._thread is not None
|
|
assert m._thread.is_alive()
|
|
time.sleep(0.1)
|
|
m.stop()
|
|
assert not m._thread.is_alive()
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
def test_draw_calls_stats_collector(self) -> None:
|
|
collector = _make_collector()
|
|
call_count = 0
|
|
original_get = collector.get_current_stats
|
|
|
|
def counting_get():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return original_get()
|
|
|
|
collector.get_current_stats = counting_get # type: ignore[assignment]
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = io.StringIO()
|
|
try:
|
|
m = ProgressMonitor(collector)
|
|
m.start()
|
|
time.sleep(1.5) # allow at least one tick
|
|
m.stop()
|
|
assert call_count >= 1
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
def test_stop_without_start(self) -> None:
|
|
old_stdout = sys.stdout
|
|
sys.stdout = io.StringIO()
|
|
try:
|
|
m = ProgressMonitor(_make_collector())
|
|
m.stop() # should not raise
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thread safety
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestProgressMonitorThreadSafety:
|
|
def test_concurrent_send_finish_log(self) -> None:
|
|
collector = _make_collector()
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = io.StringIO()
|
|
try:
|
|
m = ProgressMonitor(collector)
|
|
m.start()
|
|
|
|
num_threads = 10
|
|
ops_per_thread = 50
|
|
barrier = threading.Barrier(num_threads)
|
|
|
|
def worker(thread_id: int) -> None:
|
|
barrier.wait()
|
|
for i in range(ops_per_thread):
|
|
rid = f"t{thread_id}_r{i}"
|
|
m.on_request_sent(rid)
|
|
m.log_message(f"sent {rid}")
|
|
m.on_request_finished(rid, True)
|
|
|
|
threads = [
|
|
threading.Thread(target=worker, args=(t,)) for t in range(num_threads)
|
|
]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
m.stop()
|
|
# All sends matched by finishes → in-flight == 0
|
|
assert m._in_flight == 0
|
|
finally:
|
|
sys.stdout = old_stdout
|